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>
Related
I'm attempting to replicate the content in a particular IFrame element inside of a modal to avoid unnecessary DB calls. I am invoking a clientside callback via Python (see here) that returns the index of a particular IFrame element I would like to replicate in my modal.
Here is the snippet of Python code that toggles my modal and tracks the index of the most recently clicked figure to replicate:
#app.callback(
[Output('my-modal', 'is_open'),
Output('modal-clone', 'children')],
[Input(f'button{k}', 'n_clicks_timestamp') for k in range(20)] +
[State('my-modal', 'is_open')])
def toggle_modal(*data):
clicks, is_open = data[:20], data[20]
modal_display = not is_open if any(clicks) else is_open
clicked = clicks.index(max(clicks))
return [modal_display, clicked]
app.clientside_callback(
ClientsideFunction(namespace='clientside', function_name='clone_figure'),
Output('modal-test', 'children'),
[Input('modal-clone', 'children'), Input('modal-figure', 'id')]
)
And the following Javascript:
window.dash_clientside = Object.assign({}, window.dash_clientside, {
clientside: {
clone_figure: function(clone_from, clone_to) {
source = document.getElementById(clone_from);
console.log(document.getElementById(clone_to))
console.log(document.getElementById(clone_to).contentDocument);
clone = document.getElementById(clone_to);
// set attributes of clone here using attributes from source
return null
}
}
});
Now, from my console.log() statements, I noticed the following (note that modal-clone in the screenshot corresponds to modal-figure in my example):
How is contentDocument changing between these two log statements? Any insight would be greatly appreciated, I am stumped.
It appears that you need to addEventListener() to the IFrame element:
clone_spray: function(clone_from, clone_to) {
source = document.getElementById(clone_from);
clone = document.getElementById(clone_to);
if (!clone) {return null;}
clone.addEventListener("load", function() {
// set attributes of clone here using attributes from source
I have deferred updates enabled.
I have two components.
The first is a list, which is simply implemented as a div with a foreach data binding:
<div class="list-people" data-bind="foreach: { data: people, afterRender: afterRenderPeople }">
<!-- ko component: { name: "listitem-person", params: { person: $data } } --><!-- /ko -->
</div>
The second is the list item:
<div class="listitem-person">
<span data-bind="text: Name"></span>
</div>
afterRender is called for each item in the foreach.
My afterRenderPerson function is simple enough:
public afterRenderPerson = (elements: any[], data: Person) => {
let top = $(element[0]).offset().top;
scrollTo(top);
};
The problem is that when afterRenderPerson is called the sub-component listitem-person hasn't yet been rendered.
Which means the element array passed to afterRenderPerson has 4 nodes:
A text node containing \n i.e. a new line.
A comment node containing <!-- ko component: { name: "listitem-person", params: { person: $data } } -->.
A comment node containing <!-- /ko -->.
A text node containing \n i.e. a new line.
None of these are suitable for getting the top pixel, and even if they were, the sub-component being rendered could affect the layout at that location changing the value of the pixel I'm trying to get.
Unfortunately it seems that the documentation for foreach doesn't take in to account the delayed nature of components.
If you need to run some further custom logic on the generated DOM elements, you can use any of the afterRender/afterAdd/beforeRemove/beforeMove/afterMove callbacks described below.
Note: These callbacks are only intended for triggering animations related to changes in a list.
There are two workarounds I've come across, neither of which are great, but that's why they're workarounds and not solutions!
user3297291 gave the suggestion in a comment of making a scrollTo binding that's placed on the child components.
Only workaround I can think of is to define a custom scrollTo binding and include it in the component template... Quite easy to implement, but still feels hacky and makes your inner component harder to reuse. You might also want to track this feature request – user3297291
This would simply be a custom binding that conditionally executes some code based on a value provided to it.
The bindings aren't called until the HTML has been inserted in to the DOM. That's not perfect, as later changes to the DOM could affect the position of the inserted HTML elements, but it should work for many situations.
I wasn't very keen on having to modify the child components though, I preferred a solution when remained encapsulated in the parent component.
The second workaround is to check to see if the child component HTML element exists in the DOM by it's ID. Since I don't know when they will come in to existence this has to be done in some sort of loop.
A while loop isn't suitable as it'll run the check far too often, in a "tight" loop, so instead setTimeout is used.
setTimeout is a horrid hack, and it makes me feel dirty to use it, but it does work for this situation.
private _scrollToOffset = -100;
private _detectScrollToDelayInMS = 200;
private _detectScrollToCountMax = 40;
private _detectScrollToCount = 0;
private _detectScrollTo = (scrollToContainerSelector: string, scrollToChildSelector: string) => {
//AJ: If we've tried too many times then give up.
if (this._detectScrollToCount >= this._detectScrollToCountMax)
return;
setTimeout(() => {
let foundElements = $(scrollToChildSelector);
if (foundElements.length > 0) {
//AJ: Scroll to it
$(scrollToContainerSelector).animate({ scrollTop: foundElements.offset().top + this._scrollToOffset });
//AJ: Give it a highlight
foundElements.addClass("highlight");
} else {
//AJ: Try again
this._detectScrollTo(scrollToContainerSelector, scrollToChildSelector);
}
}, this._detectScrollToDelayInMS);
this._detectScrollToCount++;
};
I made sure to put a limit on how long it can run for, so if something goes wrong it won't loop forever.
It should probably be noted that there is an "Ultimate" solution to this problem, and that's TKO, AKA Knockout 4.
But that's not "production ready" yet.
How to know when a component has finished updating DOM?
brianmhunt commented on Jun 20
knockout/tko (ko 4 candidate) latest master branch has this.
More specifically, the applyBindings family of functions now return a Promise that resolves when sub-children (including asynchronous ones) are bound.
The API isn't set or documented yet, but the bones have been set up.
This appears to work. I made a binding handler that runs a callback in its init (it uses tasks.schedule to allow a rendering cycle). Attaching it at the parent level does not get the children rendered in time, but attaching it to the virtual element does.
I designed it to work with a function whose signature is like afterRender. Because it runs for each of the elements, the callback function has to test that the data is for the first one of them.
ko.options.deferUpdates = true;
ko.bindingHandlers.notify = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
// Make it asynchronous, to allow Knockout to render the child component
ko.tasks.schedule(() => {
const onMounted = valueAccessor().onMounted;
const data = valueAccessor().data;
const elements = [];
// Collect the real DOM nodes (ones with a tagName)
for(let child=ko.virtualElements.firstChild(element);
child;
child=ko.virtualElements.nextSibling(child)) {
if (child.tagName) { elements.push(child); }
}
onMounted(elements, data);
});
}
};
ko.virtualElements.allowedBindings.notify = true;
function ParentVM(params) {
this.people = params.people;
this.afterRenderPeople = (elements, data) => {
console.log("Elements:", elements.map(e => e.tagName));
if (data === this.people[0]) {
console.log("Scroll to", elements[0].outerHTML);
//let top = $(element[0]).offset().top;
//scrollTo(top);
}
};
}
ko.components.register('parent-component', {
viewModel: ParentVM,
template: {
element: 'parent-template'
}
});
function ChildVM(params) {
this.Name = params.person;
}
ko.components.register('listitem-person', {
viewModel: ChildVM,
template: {
element: 'child-template'
}
});
vm = {
names: ['One', 'Two', 'Three']
};
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<template id="parent-template">
<div class="list-people" data-bind="foreach: people">
<!-- ko component: { name: "listitem-person", params: { person: $data } }, notify: {onMounted: $parent.afterRenderPeople, data: $data} -->
<!-- /ko -->
</div>
</template>
<template id="child-template">
<div class="listitem-person">
<span data-bind="text: Name"></span>
</div>
</template>
<parent-component params="{ people: names }">
</parent-component>
My Problem:
I am trying to click options in a dropdown with Nightwatch, using sections in page objects. I'm not sure if it's a problem with the section declaration or i'm missing something scope-related. Problem is that it finds the element as visible, but when it tries to click it will throw error that it cannot locate it using recursion.
What could i try to do to fix this issue using sections?
In the test:
var myPage = browser.page.searchPageObject();
var mySection = searchPage.section.setResults;
// [finding and clicking the dropdown so it opens and displays the options]
browser.pause (3000);
browser.expect.section('#setResults').to.be.visible.before(1000);
myPage.myFunction(mySection, '18');
In the page object:
var searchKeywordCommands = {
myFunction: function (section, x) {
section.expect.element('#set18').to.be.visible.before(2000);
if (x == '18') section.click('#set18');
//[...]
};
module.exports = {
//[.. other elements and commands..]
sections: {
setResults: {
selector: '.select-theme-result', //have also tried with '.select-content' and '.select-options' but with the same result
elements: {
set18: '.select-option[data-value="18"]',
set36: '.select-option[data-value="36"]' //etc
}}}}
Here is my source code:
When i run this piece of core, it seems to find the section, finds the element visible (i also can clearly see that it opens the dropdown and shows the options) but when trying to click any option, i get the error: ERROR: Unable to locate element: Section[name=setResults], Element[name=#set18]" using: recursion
Here is the full error:
My attempts:
I have tried to declare that set18 selector as an individual element instead of inside of the section and everything works fine this way, but won't work inside of the section. I have also tried all the selectors available to define the section's selector, but it won't work with any of them.
This is what i am doing with(LOL)
I assume steps would be (find dropbox - click dropbox - select value).
var getValueElement = {
getValueSelector: function (x) {
return 'li[data-value="'+ x + '"]';
}
};
module.exports = {
//[.. other elements and commands..]
sections: {
setResults: {
commands:[getValueElement],
selector: 'div[class*="select-theme-result"', //* mean contains,sometime class is too long and unique,also because i am lazy.
elements: {
setHighlight:'li[class*="select-option-highlight"]',
setSelected:'li[class*="select-option-selected"]',
//set18: 'li[data-value="18"]',
//set36: 'li[data-value="36"]'
// i think getValueFunction is better,what if you have 100+ of set.
}}}}
In your test
var myPage = browser.page.searchPageObject();
var mySection = searchPage.section.setResults;
// [finding and clicking the dropdown so it opens and displays the options]
mySection
.click('#dropboxSelector')
.waitForElementVisible('#setHighlight',5000,false,
function(){
var set18 = mySection.getValueElement(18);
mySection.click(set18);
});
Ps:in my case(i think your case also), dropbox or any small third-party js framework which is used many times in your web app, so better create a different PageObject for it,make pageObject/section is simple as possible.
I'm trying to create my own lightbox script where I can pass the variables (title, description, itemtype, itemid, etc.) in clean formatting like this (inspired by fancybox):
myFunction({
title: "My title",
description: "My description"
});
Clicking on a certain element prepends some HTML to a div with jQuery.
I have adapted a piece of code I found on Stackoverflow and "kind of" understand the code. The top function has not been changed and worked before I edited the bottom code, to that I added click(function() { } because in the example the code was executed on pageload.
However, when I click my H1 element the firebug console tells me ReferenceError: popup is not defined
This is my Javascript:
$(document).ready(function() {
(function ($) {
$.fn.popup = function (options) {
var settings = $.extend({
title: function (someData) {
return someData;
},
description: function (someData) {
return someData;
},
}, options);
$("#content").prepend(
"<div style=\"position:fixed;top:0;left:0;width:100%;height:100%;background:#FFFFFF;\">\
<h1>"+ settings.title +"</h1>\
<p>" + settings.description +"</p>\
</div>"
);
};
}(jQuery));
$(".openbox1").click(function() {
popup({
title: "Title 1",
description: "Description 1"
});
}));
$(".openbox2").click(function() {
popup({
title: "Title 2",
description: "Description 2"
});
}));
});
This is my HTML
<div id="content">
<h1 class="openbox1">open box 1</h1>
<h1 class="openbox2">open box 2</h1>
</div>
A. Wolff commented that I need to execute the function like this:
$(".openbox1").click(function() {
$(this).popup({
...
});
});
This fixed it, thanks!
First off, what you did, and I hope this helps:
// This, of course is same as "document.onload"
// Don't confuse it with "window.onload"
// wich will wait till WHOLE dom is loaded to run any script
$(document).ready(function() {
(function ($) {
// This is, in essence, the start of a jQuery plugin
// This is often referred to as the "quick and dirty setup"
// as it's a direct call to add a method to jQuery's
// element object. Meaning it can be recalled as
// $(element).popup().
// This should not be confused with $.popup = function
// which would just add a method to jQuery's core object
$.fn.popup = function (options) {
var settings = $.extend({
...
}(jQuery));
$(".openbox1").click(function() {
// here is where your issue comes in
// as previously noted, you did not create a
// method named "popup".
// you added a method to jQuery's Element Object
// called "popup".
// This is why `$(this).popup` works and
// plain `popup` does not.
// You're inside an "event" asigned to any element
// having class name `openbox1`. Thus, any call
// in here to `this`, will reference that element
popup({
Secondly, a different example of how to write it. I won't say better because, even if I say my way is better, it wouldn't make your "corrected" way wrong. In Javascript, as the old saying goes, There's more than one way to skin a cat.
My Example:
// Notice I'm adding this plugin BEFORE the document load.
// This means, you could easily add this to a file and load it
// in script tags like any other Javascript,
// as long as it's loaded AFTER jquery.
(function($) {
// this ensures that your plugin name is available and not previously added to jQuery library
if (!$.popup) {
// this also provides us "variable scope" within to work in
// here begin adding the plugin to jQuery
// I started with $.extend, so it can be added to the jQuery library and used in traditional format
// $.popup('element selector', { options })
// as well as the element.action format we'll add later
// $.(element selector).popup({ options })
// This should help give you a good idea of the whole of what all is going on
$.extend({
popup: function() {
var ele = arguments[0], // this is our jQuery element
args = Array.prototype.slice.call(arguments, 1); // this gets the rest of the arguments
// this next step is useful if you make the traditional call `$.popup(this, { options })`
if (!(ele instanceof jQuery)) ele = $(ele);
// now we have total control! Bwahahha!
// Fun aside, here is where it's good to check if you've already asigned this plugin
// if not, then make some "marker", so you can recall the element plugin and comment an
// action instead of reinitializing it
if (!ele.data('popup')) $.popup.init(ele, args);
else {
// at this point, you would know the element already has this plugin initialized
// so here you could change an initial options
// like how with jQueryUI, you might would call:
// $(element).popup('option', 'optionName', value)
}
return ele;
}
});
// here is where we add the $(element selector).popup method
// this simply adds the method to the element object
// If you don't fully understand what's going on inside (as I explain below),
// just know that it's some "fancy footwork" to pass the method onto our initial
// method creation, $.popup
$.fn.extend({
popup: function(/*no need for parameter names here as arguments are evaluated inside and passed on to initial method*/) {
// set this element as first argument to fit with initial plugin method
var args = [$(this)];
// if there are arguments/params/options/commands too be set, add them
if (arguments.length) for (x in arguments) args.push(arguments[x]);
// pass through jquery and our arguments, end result provides same arguments as if the call was:
// $.popup($(element), options)
return $.popup.apply($, args);
}
});
// This next part is not seen in many plugins but useful depending on what you're creating
$.popup.init = function(ele, opt) {
// here is where we'll handle the "heavy work" of establishing a plugin on this element
// Start with setting the options for this plugin.
// This means extending the default options to use any passed in options
// In the most simple of cases, options are passed in as an Oject.
// However, that's not always the case, thus the reason for this being
// a continued array of our arguments from earlier.
// We'll stick with the simplest case for now, your case, that the only options are an
// Object that was passed in.
// using the extend method, with true, with a blank object,
// allows us to added the new options "on top" of the default ones, without changing the default ones
// oh and the "true" part just tells extend to "dig deep" basically (multideminsional)
if (opt && typeof opt[0] == 'object') opt = $.extend(true, {}, $.popup.defaults, opt[0]);
var par = opt.parent instanceof jQuery ? opt.parent : $('body'),
tit = opt.title,
des = opt.description,
// this last one will be the wrapper element we put everything in
// you have this in yours, but it's written in a very long way
// this is jQuery simplified
wrap = $('<div />', { style: 'position:fixed;top:0;left:0;width:100%;height:100%;background:#FFFFFF;' }),
// much like the previous element, cept this is where our title goes
head = $('<h1 />', { text: tit }).appendTo(wrap),
content = $('<p />', { text: des }).appendTo(wrap);
$(par).append(wrap);
// finally, add our marker i mentioned earlier
ele.data('popup', opt);
// just adding the following cause i noticed there is no close
// fyi, i would change this plugin a little and make an actial "open" command, but that's another tutorial
var closer = $('<span />', { text: '[x]', style: 'cursor:pointer;position:absolute;bottom:1em;right:1em;' });
wrap.append(closer);
closer.click(function(e) { ele.data('popup', false); wrap.remove(); });
};
$.popup.defaults = { // establish base properties here that can be over-written via .props, but their values should never truly change
'parent': undefined, // added this to keep it dynamic, instead of always looking for an element ID'd as content
title: '',
description: ''
};
}
})(jQuery);
// the following is basically jQuery shorthand for document.ready
$(function() {
// i think you get the rest
$(".openbox1").on('click', function(e) {
$(this).popup({
title: "Title 1",
description: "Description 1",
parent: $("#content")
});
})
$(".openbox2").on('click', function(e) {
$(this).popup({
title: "Title 2",
description: "Description 2",
parent: $("#content")
});
})
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<div id="content">
<h1 class="openbox1">open box 1</h1>
<h1 class="openbox2">open box 2</h1>
</div>
I am using Titanium Alloy version 3.2. I have a collection of posts in a listview. My data looks like this:
[
{ username: 'dude', imageUrl: 'url', tags: ['tag1','tag2','tag3'] },
{ username: 'wheres', imageUrl: 'url', tags: ['tag1'] },
{ username: 'my', imageUrl: 'url', tags: ['tag1','tag2','tag3','tag4'] },
{ username: 'car', imageUrl: 'url', tags: ['tag1','tag2'] }
]
And here is the xml. This works only for username and image. I can't figure out how to add the tags to each post.
<ListView id="streamListview">
<Templates>
<ItemTemplate name="template" id="template">
<View class="item-container">
<ImageView bindId="pic" class="pic"/>
<Label bindId="username" class="username"/>
</View>
</ItemTemplate>
</Templates>
<ListSection id="section">
<ListItem template="template" class="list-item"/>
</ListSection>
</ListView>
And my controller code (without the tags)
var posts = [];
for (var i=0; i<data.length; i++){
var post = {
template : "template",
pic : { image : data[i].get("imageUrl") },
username : { text : data[i].get("username") }
};
posts.push(post);
}
$.section.setItems(posts);
How can I add tags (that are clickable) to the post if I am supposed to declare EVERY view in the template before hand? Each tags array in my example would need a different number of views depending on the array length. Each tag would ideally be its own UI.Label element. I believe this can be done using a TableView, but I would prefer using ListView for performance reasons.
I think I know what you need, in this case since you want to generate each item dynamically (for example, a scenario where you open your window with your ListView empty first and make an API call to get remote data and fill the ListView with said data) you will need to use ItemTemplates declared in their own controllers.
You just create a new controller like normal and in the view xml you put your ItemTemplate:
<Alloy>
<ItemTemplate name="template" id="template">
<View class="item-container">
<ImageView bindId="pic" class="pic"/>
<Label bindId="username" class="username"/>
</View>
</ItemTemplate>
</Alloy>
In your tss you put all of the styles referred to each element in your template, since you didn't provide a tss example I can't tell what are your style properties, but in the tss you need to define the style of the template, for example lets say something like:
"#template": // this is the id of your template in your xml
{
width : Ti.UI.FILL,
height : '44dp',
backgroundColor : '#FFFFFF'
}
To fill your ListView with ListItems dynamically, you will need to do something like this in your callback from your API:
function displayListItems(items)
{
var itemCollection = [];
for(var i=0; i < items.length; i++)
{
var tmp = {
pic : {
image : items[i].image
},
username : {
text : items[i].text
},
template : 'template' // here goes the name of the template in your xml, **do not confuse name with id, both are different and using one doesn't replace the other**
};
itemCollection.push(tmp);
}
$.ListView.sections[0].items = itemCollection;
}
And voila, you get your ListView filled dynamically. Now there are some extra steps you can do.
In your template controller you can leave it blank since the ListView can manage the itemclick event, but if you want different actions to take place when a certain element in the Listitem to trigger, you need to specify the functions to be called in your controller for each element.
For example lets say you passed a property called dataInfo to your ImageView and your Label in your template like this:
function displayListItems(items)
{
var itemCollection = [];
for(var i=0; i < items.length; i++)
{
var tmp = {
pic : {
image : items[i].image
dataInfo : items[i].fooA //lets pass to the ImageView the object fooA
},
username : {
text : items[i].text,
dataInfo : items[i].fooB //lets pass to the Label the object fooB
},
template : 'template' // here goes the name of the template in your xml, **do not confuse name with id, both are different and using one doesn't replace the other**
};
itemCollection.push(tmp);
}
$.ListView.sections[0].items = itemCollection;
}
And you want the ImageView and the Label to call different functions, you will need to change your xml like this:
<Alloy>
<ItemTemplate name="template" id="template">
<View class="item-container">
<ImageView bindId="pic" class="pic" onClick="imageFunction"/> <!-- added onClick event -->
<Label bindId="username" class="username" onClick="labelFunction"/> <!-- added onClick event -->
</View>
</ItemTemplate>
</Alloy>
In your controller you will declare each function:
function imageFunction(e)
{
var dataInfo;
if(Ti.Platform.osname === 'android')
{
var item = e.section.items[e.itemIndex];
var bindObject = item[e.bindId];
dataInfo = bindObject.fooA;
}
else
{
dataInfo = e.source.fooA;
}
}
function labelFunction(e)
{
var dataInfo;
if(Ti.Platform.osname === 'android')
{
var item = e.section.items[e.itemIndex];
var bindObject = item[e.bindId];
dataInfo = bindObject.fooB;
}
else
{
dataInfo = e.source.fooB;
}
}
Now you might ask, why do check for the operative system name, well that is because Android and iOS receive different e objects even if you use the same function. In iOS whatever property you pass to the source of the event can be accessed directly with e.source.propertyName while in Android you need to access to the item in e.section using e.itemIndex, after that you retrieve the view inside the item with the e.bindId associated to it.
One of the biggest restrictions on ListItems is updating the views inside a ListItem, to do this you need to update the whole item you want to change visually and assign it a different template, but the speed at which this is done you won't be able to notice any lag, seriously ListView's performance is something else, unlike ScrollView and let's not talk about the horrible and buggy TableView.
A warning, as of Titanium SDK 3.2.0.GA there's a bug in ItemTemplates that causes for views inside child views in the template to change their zIndex in Android with no way to control it, there are two known instances for this:
If you use a don't set the layout in a child view: this could cause for a view that should be displayed beneath another view to come on top of it.
If you use a vertical layout in a child view: this could cause for the positions of each view to be scrambled, this is because zIndex alters the order of display in a vertical layout.
This bug is triggered randomly and the Appcelerator team hasn't put much work on it, check the JIRA ticket here TIMOB-16704.
This can be avoided if you use a template with fixed positioned views and making sure no view comes on top of another, also remember no vertical layouts, haven't tested this with horizontal but personally I try to avoid horizontal layouts since there are other bugs related to it when used in scrollviews, normal views, etc.
EDIT
Another thing you might want to do with this is to assign a different look to the items you render, you have to options:
To apply the styles when you declare the ListItem.
To apply a different layout to each ListItem depending on a series of conditions.
For the first option you need to omit or overwrite the declaration of certain properties in your template:
For example, let's use a different background color where the property fooA exists and another color if it doesn't:
function displayListItems(items)
{
var itemCollection = [];
for(var i=0; i < items.length; i++)
{
var properties = {};
if(typeof items[i].fooA !== 'undefined')
{
properties = {
backgroundColor : 'red'
};
}
else
{
properties = {
backgroundColor : 'blue'
};
}
var tmp = {
properties : properties, // properties for the template
pic : {
image : items[i].image
dataInfo : items[i].fooA //lets pass to the ImageView the object fooA
},
username : {
text : items[i].text,
dataInfo : items[i].fooB //lets pass to the Label the object fooB
},
template : 'template' // here goes the name of the template in your xml, **do not confuse name with id, both are different and using one doesn't replace the other**
};
itemCollection.push(tmp);
}
$.ListView.sections[0].items = itemCollection;
}
You can change width, height, backgroundColor, layout, etc. according to your needs.
Now if you want each item to have a distinct look (meaning different views to display different content) and perhaps a different behavior, you'll need to use different templates.
This might sound bothersome but it is not, templates are fast to create once you get used to them which doesn't take long, another downer might be that if you want 11 different looks, that might mean you'll need 11 templates but that's a extreme case and you might want to rethink your UI if you're dealing with that many templates.
Although restrictive, item templates offer a wide array of options for you to use, a little of imagination is the only ingredient necessary to bring out all of the possibilities.
EDIT 2
I finally understood what was you problem, if you need to create a template whose content changes according to a x variable, then you should try declaring the template on your ListView controller, but this should be done before opening the window were you will be showing the ListView since the templates property can only be set on creation, you should add something like:
function createTemplate(items)
{
var template = {};
for(var i=0; i < items.length; i++)
{
template.childTemplates = [];
for(var j=0; items[i].tags.length; j++)
{
var childTemplate = {
type: 'Ti.UI.Label',
bindId: 'tag' + j,
properties : {
width : Ti.UI.SIZE, // Here you define how your style
height : Ti.UI.SIZE,
font : {
fontSize : '18dp'
},
text : items[i].tags[j].text // you can pass the text here or if you want to on the later for
}
};
template.childTemplates.push(childTemplate);
}
}
// After this you should end up with a template with as many childTemplates as tags each item have, so send the template to the controller with your ListView
Alloy.createController('ListViewWindow', {customTemplate: template});
}
And in your ListView controller you retrieve the template:
var args = arguments[0] || {};
var template = args.customTemplate;
$.ListView.templates = {'customTemplate' : template}; // before any window open call, do this
This should add the template to your ListView, you can also create the ListView in your controller instead of declaring it in your Alloy xml, use the one that fits your needs more.
This should be possible with a ListView if you create the template in the controller dynamically. You would also need to iterate through each "tags" object and generate a Ti.UI.Label "type" for each tag item. However, I'm not certain this method will be more efficient than using a TableView object because essentially every ListItem you create will contain a different template.
To generate a dynamic template it would be similar to this below: Keep in mind you will need to iterate over "tags" and generate x Ti.UI.Label types where x is the length of "tags". Also, the click event should work using Titanium SDK 3.2.1.
var plainTemplate = {
childTemplates: [
{
type: 'Ti.UI.Label',
bindId: 'username'
},
{
type: 'Ti.UI.ImageView',
bindId: 'pic'
},
{
type: 'Ti.UI.Label',
bindId: 'tags',
events: { click : handleTagClickEvent } // Binds a callback to click event
}
]};
function handleTagClickEvent(e) {
Ti.API.info('You clicked a tag label: ' + e.type);
}
var listView = Ti.UI.createListView({
templates: { 'plain': plainTemplate },
defaultItemTemplate: 'plain'
});
Hope this helps you in some way!