Create a Reusable HTML Control w/ Javascript - javascript

So, I've been searching through some existing questions dealing with re-usable items in HTML and Javascript, and I'm not sure if there's anything that gives me the start I'm looking for. I'm not super well-versed in js, but rather than re-write the same code over and over again and have to perform the upkeep on it, I'd prefer to build a re-usable framework that I can apply in several places.
The basic layout is this: There's an input field with an "Add" button, each time you add a name, it displays below the input with a checkbox. When you uncheck it, the name is removed from the list.
I'm fine with styling and building the HTML, what I'm lost on is developing an object in js that I can apply in multiple places. What I had in mind was this:
function createInputControl(targetElementId) {
var newInputControl = new ItemInputControl();
newInputControl.onItemAdded = customItemAddedCallback;
newInputControl.onItemRemoved = customItemRemovedCallback;
newInputControl.createInElement(targetElementId);
}
That's the start I'm looking for. An object that I can create that has designated callbacks for when an item is added or removed via user interaction, and a way for me to draw it within an existing element on my page.
EDIT: What I'm looking for here is a skeleton of a javascript object (named ItemInputControl above) with these functions / properties that I can re-use throughout my site.

Ok, so If I understand you correctly - you're looking for help on how to make a globally accessible variable that can be used in your entire application, like jQuery. You have two main options for what you are looking to do
First - you could use an Object Literal, which exposes a single global variable and all of your methods are contained within:
(function (window) {
var inputControl = {
onItemAdded: function () {
// do stuff
},
onItemRemoved: function () {
// do stuff
},
createInElement: function (targetElementId) {
// do stuff
}
};
window.ItemInputControl = inputControl;
})(window);
This is used like so:
ItemInputControl.createInElement("elementId");
Your second option is to use Prototype:
(function (window) {
var inputControl = function () {
// Constructor logic
return this;
};
inputControl.prototype.onItemAdded = function () {
// do stuff
return this;
};
inputControl.prototype.onItemRemoved = function () {
// do stuff
return this;
};
inputControl.prototype.createInElement = function (elementId) {
// do stuff
return this;
};
window.ItemInputControl = inputControl;
})(window);
This would be used like so:
var newInputControl = new ItemInputControl();
newInputControl.createInElement("elementId");
For most cases in individual applications - I prefer to use Object Literals for my framework. If I were building a widely distributed javascript framework, I would probably use a prototype pattern. You can read more on prototype patters here: http://www.htmlgoodies.com/beyond/javascript/some-javascript-object-prototyping-patterns.html

Well, I'm not sure if this is exactly helpful, but perhaps it will contain a few ideas for you.
The two HTML elements needed are stored as format strings, and everything is dynamically added/removed in the DOM.
var listid = 0;
$(document).ready(function() {
var controlHtml = + // {0} = mainid
'<div>' +
'<input id="text-{0}" type="text" />' +
'<div id="add-{0}" class="addButton">Add</div>' +
'</div>' +
'<div id="list-{0}"></div>';
var listHtml = + // {0} = mainid, {1} = listid, {2} = suplied name
'<div id="list-{0}-{1}"><input id="checkbox-{0}-{1}" type="checkbox class="checkboxClass" checked />{2}<div>';
$('#content').append(controlHtml.f('0'));
$('.addButton').click(function(e) { addClick(e); });
});
function addClick(e) {
var id = e.currentTarget.id.split('-')[1];
var name = $('text-' + id).val();
$('.list-' + id).append(listHtml.f(id, listid, name));
listid++;
$('.checkboxClass').click(function() { checkboxClick(e); });
}
function checkboxClick(e) {
$('#' + e.currentTarget.id).remove();
}
String.prototype.f = function () { var args = arguments; return this.replace(/\{(\d+)\}/g, function (m, n) { return args[n]; }); };
And of course very minimal HTML to allow a hook for adding your control:
<body>
<div id="content"></div>
</body>

Related

Swapping hardcoded arguments for variables

I've got a functional script on my site that allows me to open a link in a new window when a specific class is added to the link. I need a lot of those on my site though so I figured I'd make the script a bit easier to edit by working with variables.
In the process of changing out hardcoded strings for variables my script stopped working though. The only one that works is the var where I set the url.
I'm learning that ${} doesn't work everywhere. Hope that someone can point out where my thinking is wrong. Also hope that I got the terminology right, trying to learn though! :-)
var function1Name = "test_function";
var function1Url = "https://www.google.com";
var function1Class = ".test_function_class";
function ${function1Name}() {
window.open(function1Url, "_blank", "height=200");
}
jQuery("${function1Class}").click(function(){
${function1Name}()
});
None of your uses of ${} are valid JavaScript syntax.
Your function declaration van be replaced with:
window[function1Name] = function () {
window.open(function1Url, "_blank", "height=200");
}
Please note that the function will no longer be hoisted when declared this way, so order of operation matters.
The click handler can be written as follows:
jQuery(function1Class).click(function() { // Note that I just used the variable there.
window[function1Name]();
});
There is a ${} concept in JavaScript, but that is only in template literals:
const myVariable = "Foo";
const message = `myVariable contains: "${myVariable}"!`;
console.log(message);
There's several syntax issues here.
Firstly, function ${function1Name}() is not valid syntax. Function names must be defined before runtime. If you want to dynamically access a function, place it in an object and set the key with the variable reference.
Secondly, ${function1Name}() is again not valid syntax. You cannot invoke a function like that dynamically. Referring to the suggestion above, you can access an object dynamically so the first point fixes this problem.
Thirdly, string interpolation only works within template literals, so you need to delimit the string with backticks: ``. However it's completely redundant here as you can just use $(function1Class)
With those issues in mind, here's an updated example:
var function1Name = "test_function";
var function1Url = "https://www.google.com";
var function1Class = ".test_function_class";
var funcObj = {
[function1Name]: function() {
console.log(`function called, window would open here containing ${function1Url}...`);
// window.open(function1Url, "_blank", "height=200");
}
}
$(function1Class).click(function() {
funcObj[function1Name]()
});
/*
alternative using a template literal, although note that it's redundant here
$(`${function1Class}`).click(function() {
funcObj[function1Name]()
});
*/
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
Click me
One last thing to note is that no version of IE supports template literals, so be sure of your browser support requirements before using them.
So cool, I got it to work!
var function1Name = "test_function_1";
var function1Url = "https://www.google.com";
var function1Class = ".test_function_class1";
var function2Name = "test_function_2";
var function2Url = "https://www.cnn.com";
var function2Class = ".test_function_class2";
// Function 1
window[function1Name] = function () {
window.open(function1Url, "_blank", "toolbar=no,status=no,scrollbars=yes,resizable=yes,top=500,left=500,width=600,height=745");
}
jQuery(function1Class).click(function() {
window[function1Name]();
});
// Function 2
window[function2Name] = function () {
window.open(function2Url, "_blank", "toolbar=no,status=no,scrollbars=yes,resizable=yes,top=500,left=500,width=600,height=745");
}
jQuery(function2Class).click(function() {
window[function2Name]();
});
I can now add a bunch of url's and corresponding classes as was my intention. Super happy about that.
A follow up question though, as I'll have to experiment with what the ideal window parameters will be I'm trying to make those arguments variables as well. I've tried the examples of how to insert a variables output from the functional code but those methods don't work there. Here's that code:
var windowWidth = 250
var function1Name = "test_function_1";
var function1Url = "https://www.google.com";
var function1Class = ".test_function_class1";
var function2Name = "test_function_2";
var function2Url = "https://www.cnn.com";
var function2Class = ".test_function_class2";
// Function 1
window[function1Name] = function () {
window.open(function1Url, "_blank", "toolbar=no,status=no,scrollbars=yes,resizable=yes,top=500,left=500,width=[windowWidth],height=745");
}
jQuery(function1Class).click(function() {
window[function1Name]();
});
// Function 2
window[function2Name] = function () {
window.open(function2Url, "_blank", "toolbar=no,status=no,scrollbars=yes,resizable=yes,top=500,left=500,width=600,height=745");
}
jQuery(function2Class).click(function() {
window[function2Name]();
});
How would I insert the variables value (2nd line of Function1) there ?

Using Knockout bindings to return string

I'm trying to use Knockout to make the usage for an infinite scroll plugin I am using a bit nicer, but struggling with how to bind it. Not sure if it's even possible in the current form.
The scroller calls a data function which loads the next block of data via AJAX. It then calls a factory function that converts that data into HTML, and it then loads the HTML into the container, and updates its internal state for the current content size.
I'm stuck on the fact that it expects an HTML string.
What I want to do is this:
<div class="scroller" data-bind="infiniteScroll: { get: loadItems }">
<div class="item">
<p>
<span data-bind="text:page"></span>
<span class="info" data-bind="text"></span>
</p>
</div>
</div>
And my binding, which I'm completely stuck on, is this - which is currently just hardcoding the response, obviously - that's the bit I need to replace with the template binding:
ko.bindingHandlers.infiniteScroll = {
init:
function(el, f_valueaccessor, allbindings, viewmodel, bindingcontext)
{
if($.fn.infiniteScroll)
{
// Get current value of supplied value
var field = f_valueaccessor();
var val = ko.unwrap(field);
var options = {};
if(typeof(val.get) == 'function')
options = val;
else
options.get = val;
options.elementFactory = options.elementFactory ||
function(contentdata, obj, config)
{
var s = '';
for(var i = 0; i < contentdata.length; i++)
{
var o = contentdata[i];
// NEED TO REPLACE THIS
s += '<div class="item"><p>Item ' + o.page + '.' + i + ' <span class="info">' + o.text + '</span></p></div>';
}
return s;
};
$(el).infiniteScroll(options);
return { controlsDescendantBindings: true };
}
}
};
contentdata is an array of objects e.g. [ { page:1, text:'Item1' }, { page:1, text:'Item2' } ... ]
Page sizes may differ between calls; I have no way of knowing what the service will return; it is not a traditional page, think of it more as the next block of data.
So in the element factory I want to somehow bind the contentdata array using the markup in .scroller as a template, similar to foreach, then return that markup to the scroller plugin.
Note that I can modify the infinite scroller source, so if if can't be done with strings, returning DOM elements would also be fine.
I just can't get how to a) use the content as a template, and b) return the binding results to the plugin so it can update its state.
NOTE: The page I eventually intend to use this is currently using a foreach over a non-trivial object model; thus the need to use the same markup; it needs to be pretty much a drop in replacement.
I have actually found out how to do it using the existing scroller following this question: Jquery knockout: Render template in-memory
Basically, you use applyBindingsToNode(domelement, bindings), which will apply KO bindings to a nodeset, which importantly does not have to be connected to the DOM.
So I can store the markup from my bound element as the template, then empty it, then for the element factory, create a temporary node set using jQuery, bind it using the above function, then return the HTML.
Admittedly, this would probably be better off refactored to use a pure KO scroller, but this means I can continue to use the tested and familiar plugin, and the code might help people as this seems to be quite a common question theme.
Here is the new code for the binding (markup is as above).
ko.bindingHandlers.infiniteScroll = {
init:
function(el, f_valueaccessor, allbindings, viewmodel, bindingcontext)
{
if($.fn.infiniteScroll)
{
// Get current value of supplied value
var field = f_valueaccessor();
var val = ko.unwrap(field);
var options = {};
if(typeof(val.get) == 'function')
options = val;
else
options.get = val;
var template = $(el).html();
options.elementFactory = options.elementFactory ||
function(contentdata, obj, config)
{
// Need a root element for foreach to use as a container, as it removes the root element on binding.
var newnodes = $('<div>' + template + '</div>');
ko.applyBindingsToNode(newnodes[0], { foreach: contentdata });
return newnodes.html();
};
$(el)
.empty()
.infiniteScroll(options);
return { controlsDescendantBindings: true };
}
}
};

Javascript error: $(...).observe is not a function

I'm trying to use Stitch onEvent mixin in Tapestry 5.3.7.
Here's the OnEvent Javascript from there:
T5.extendInitializers({
onEvent: function (spec) {
var element = $(spec.id).observe(spec.event, function () {
var params = {};
if (spec.fieldIds) {
for (var i = 0; i < spec.fieldIds.length; ++i) {
var fieldId = spec.fieldIds[i];
var paramName = "onEvent." + fieldId;
var paramValue = $(fieldId).getValue();
params[paramName] = paramValue;
}
}
var zoneManager = Tapestry.findZoneManagerForZone(spec.zone);
zoneManager.updateFromURL(spec.url, params);
});
}
});
I use it in my TML like this:
<t:form t:id="filterTextForm">
<div style="float:right">
<input id="filterText" t:id="filterText" t:type="textfield"
t:value="filter" zone="configZone" t:autofocus="literal:true" onClick="this.select()" t:mixins="onEvent" event="keyup"/>
</div>
</t:form>
and my event method like this:
void onKeyupFromFilterText(String filter) {
this.filter = filter;
if (request.isXHR()) {
ajaxResponseRenderer.addRender(configZone).addRender(descZone);
}
}
When I have t:mixins="onEvent" event="keyup", none of the zones are updating on the page, no AJAX requests are generated.
When I remove it, everything works fine, except the part I'm trying to do, described in my question.
I'm using Tapestry 5.3.7.
observe() is a prototype function. I'm guessing that you're using tapestry-jquery which is a 3rd party library that removes prototype and replaces it with jquery.
You'll need to adapt the javascript to work with jquery (eg observe() needs to change to on())
You might find other bits of js broken too (eg I'm not sure if tapestry-jquery's ZoneManager is exactly the same as core tapestry).
tapestry-jquery comes with a builtin bind mixin which does a similar job to onevent. You might find it sufficient to use this mixin instead.

Dynamically Modifying Value of an Existing Event Handler

Quick background: I'm updating existing code to separate event handlers from html objects and, in an onload function I'm then assigning all necessary handlers.
$('input[name^="interactiveElement_"]').each(function() {
var fieldName = "interactiveElement_";
var elementNumber = $(this).attr("name").substring(fieldName.length,$(this).attr("name").indexOf("_",fieldName.length));
var subElementNumber = $(this).attr("name").substring((fieldName+itemNumber+'_').length,$(this).attr("name").lastIndexOf('_'));
var rowNumber = $(this).attr("name").substring($(this).attr("name").lastIndexOf('_')+1);
$(this).on("click.custNamespace", function() {
updateThisElementsMetadata(elementNumber, subElementNumber, rowNumber);
updatePreview(elementNumber);
});
});
Now for the hard part. In this interface, users will be able to trigger clones of existing elements. These clones need to have some of their handler arguments updated to new values.
Before separating out the events, I was doing that with this:
var regex = /([^0-9]+[0-9]+[^0-9a-zA-Z]+[0-9]+[^0-9a-zA-Z]+)([0-9]+)([^0-9]+)?/g;
$(element).attr("onclick",
$(element)
.attr("onclick")
.replace(regex, "$1"+newValue+"$3"));
and so on for each possible event these elements could have.
But, now using jQuery's on(event, handler) I no longer have visibility to what the handler is.
I've tried this (following this question - Get value of current event handler using jQuery):
jQuery._data(element).events.click[0].handler
But, this returns the function with variable names not values.
function () {
updateThisElementsMetadata(elementNumber, subElementNumber, rowNumber);
updatePreview(elementNumber);
}
Where I would have hoped for:
function () {
updateThisElementsMetadata(1, 2, 1);
updatePreview(1);
}
Looking in the console log I see that jQuery._data(element).events.click[0] has the values in handler => < function scope > => Closure but it doesn't seem like there is dot notation to access that, or even an array that I can dynamically cycle through.
If you tell me this isn't possible, I could change all the functions' args to just be $(this) and parse out the necessary values from it in each function, or I guess I could have a helper function... but if I could keep a similar setup to what was there it would ease other dev's learning curve.
Final Solution
To reduce duplicate code, I created a Javascript function/object that parses out necessary info from name/id tag (instead of data- attributes to reduce redundant info). Whenever an event handler is triggered it will first parse out the necessary values and then run the function w/ them.
$('input[name^="interactiveElement_"]').on("click.custNamespace", function() {
var inputField = inputFieldClass(this);
updateThisElementsMetadata(inputField.elementNumber, inputField.subElementNumber, inputField.rowNumber);
updatePreview(inputField.elementNumber);
});
var inputFieldClass = function(field) {
var fieldIdentity = $(field).attr("name") === undefined ? $(field).attr("id") : $(field).attr("name");
var fieldName = fieldIdentity.substring(0,fieldIdentity.indexOf("_")),
elementNumber = fieldIdentity.substring(fieldName.length + 1,fieldIdentity.indexOf("_",fieldName.length + 1)),
subElementNumber = fieldIdentity.substring((fieldName+'_'+elementNumber+'_').length,fieldIdentity.lastIndexOf('_')),
rowNumber = fieldIdentity.substring(fieldIdentity.lastIndexOf('_')+1);
return {
fieldName : fieldName,
elementNumber : elementNumber,
subElementNumber : subElementNumber,
rowNumber : rowNumber,
getInputName : function () {
return this.name + "_" + this.elementNumber + "_" + this.subElementNumber + "_" + this.rowNumber;
}
};
};
What I was suggesting in the comments was something like this:
$('input[name^="interactiveElement_"]').on("click.custNamespace", function() {
var fieldName = "interactiveElement_";
var elementNumber = $(this).attr("name").substring(fieldName.length,$(this).attr("name").indexOf("_",fieldName.length));
var subElementNumber = $(this).attr("name").substring((fieldName+itemNumber+'_').length,$(this).attr("name").lastIndexOf('_'));
var rowNumber = $(this).attr("name").substring($(this).attr("name").lastIndexOf('_')+1);
updateThisElementsMetadata(elementNumber, subElementNumber, rowNumber);
updatePreview(elementNumber);
});
Also, instead of parsing everything from the element's name, you could use data- attributes to make it cleaner (e.g. data-element-number="1", etc.).

How do I create methods for an HTML element?

I'm trying to create a simple, small and basic javascript framework just for learning purposes.
But the thing is that i'm allready stuck at the very basics.
I'm trying to do something like this:
$('testdiv').testFunction();
And the code i've written for that:
var elementID;
var smallFramework = {
$:function(id) {
this.elementID = id;
},
testFunction:function() {
alert(this.elementID);
}
};
window.$ = smallFramework.$;
But in return I get:
$('testdiv) is undefined
Can anyone help me with this small and hopefully easy question?
To get the behavior you're expecting, you need the $ function to return an object with a method named testFunction.
Try:
var smallFramework = // an object for namespacing
{
$:function(id) // the core function - returns an object wrapping the id
{
return { // return an object literal
elementID: id, // holding the id passed in
testFunction: function() // and a simple method
{
alert(this.elementID);
}
};
}
};
Of course, there are many other ways to achieve the behavior you desire.
If you're trying to add methods to an HTML element you could do something along these lines.
$ = function( elementId ) {
var element = document.getElementById( elementId );
element.testFunction = function(){
alert( this.id );
return this; // for chaining
}
return element;
}
$('test').testFunction();
Try
smallFramework.$('testdiv');
instead. According to the code you posted, that's where your $ function ended up.
Or alternatively, it looks like you're trying to replicate something like jQuery. You might want to try something like this.
var $ = smallFramework = (function () {
var f =
{
find:function(id) {
f.elementID = id;
return f; //every function should return f, for chaining to work
},
testFunction:function() {
alert(f.elementID);
return f;
}
}
return f.find //the find function will be assigned to $.
//and also assigned to smallFramework.
//the find function returns f, so you get access to testFunction via chaining
// like $("blah").testFunction()
})() //note this function gets called immediately.
this code may look confusing to someone new to JavaScript because it depends heavily on the concept of closures. I suggest that if this doesn't make sense, spend some time at Douglas Crockford's JavaScript website. This is important because the code above will bite if you happen to use this in the find function because this won't be bound to f, as you may expect it to be when you use it from $ or smallFramework.

Categories

Resources