Really asking this to get a better understanding of object-oriented javascript and to uncover some best practices for this scenario. Let's say I have a javascript object, such as:
SideSlider = {
rightArrow: '.controls i.right',
dot: '.controls .dot',
slide: '.slide',
init: function() {
$(this.rightArrow).click(this.nextSlide.bind(this));
$(this.leftArrow).click(this.prevSlide.bind(this));
$(this.dot).click(this.dotClick.bind(this));
},
nextSlide: function() {
var activeSlide = $('.slide.active-slide'),
firstSlide = $(this.slide).first(),
lastSlide = $(this.slide).last(),
nextUp = activeSlide.next(),
activeDot = $(".active-dot"),
nextDot = activeDot.next();
activeSlide.removeClass("active-slide");
nextUp.addClass("active-slide");
activeDot.removeClass("active-dot");
nextDot.addClass("active-dot");
$(this.leftArrow).removeClass("inactive");
if ( lastSlide.hasClass("active-slide")) {
$(this.rightArrow).addClass("inactive");
}
}
}
What is the proper way to use this object on multiple instances of DOM modules? In other words, what is the 'best-practice' way of using this object's functionality on two 'slide' instances in the same DOM
You could create a constructor for your object, and then pass a container element to that constructor, so it will be acting on that DOM-slider only. Everywhere where you perform a jQuery selector to retrieve certain element(s), you should set the scope to the given container element. You can do this by providing that container as second argument to $(..., ...).
The object instances are created with new SideSlider(container). It could look something like this:
function SideSlider(container) {
// Perform the jQuery selections with the second argument
// so that the selection returns only elements within the container:
this.$rightArrow = $('.controls i.right', container);
this.$dot = $('.controls .dot', container);
this.$slide = $('.slide', container);
this.container = container;
// ... etc
// Perform init-logic immediately
this.$rightArrow.click(this.nextSlide.bind(this));
this.$leftArrow.click(this.prevSlide.bind(this));
this.$dot.click(this.dotClick.bind(this));
// ... etc
}
// Define methods on the prototype
SideSlider.prototype.nextSlide = function() {
var activeSlide = $('.slide.active-slide', this.container),
firstSlide = $(this.slide, this.container).first(),
lastSlide = $(this.slide, this.container).last(),
nextUp = activeSlide.next(),
activeDot = $(".active-dot", this.container),
nextDot = activeDot.next();
activeSlide.removeClass("active-slide");
nextUp.addClass("active-slide");
activeDot.removeClass("active-dot");
nextDot.addClass("active-dot");
$(this.leftArrow, this.container).removeClass("inactive");
if (lastSlide.hasClass("active-slide")) {
$(this.rightArrow, this.container).addClass("inactive");
}
// ... etc
};
// Create & use the two objects:
var slider1 = new SideSlider($('#slider1'));
var slider2 = new SideSlider($('#slider2'));
// ...
slider1.nextSlide();
// ...etc.
If you have ES6 support, use the class notation.
Seeing as it looks like you are using jQuery, I would recommend looking into turning your project into a jQuery plugin. This would allow you to assign your code per use, and it's used quite commonly by developers of sliders, and other sorts of JavaScript powered widgets. The jQuery website has a great tutorial on how to accomplish this, and it can be found here:
https://learn.jquery.com/plugins/basic-plugin-creation/
Related
Having this function I need to create a new instance of it. Everything works fine in JavaScript but how to I convert it to TypeScript?
function Calendar(selector, events) {
this.el = document.querySelector(selector);
this.events = events;
this.current = moment().date(1);
this.draw();
var current = document.querySelector('.today');
if(current) {
var self = this;
window.setTimeout(function() {
self.openDay(current);
}, 500);
}
}
var calendar = new Calendar('#calendar', data);
var calendar = new Calendar('#calendar', data);
It is true that anything that works in JavaScript will work in TypeScript, but that just means that the TypeScript compiler will output your JavaScript more or less untouched, possibly spitting out a bunch of warnings on the way. If you just ignore the errors, things will still work.
But assuming you want to leverage the power of TypeScript, you should start changing things. Let's start.
First, you should install the typings from Moment.js in your project, probably by running npm install moment from your project folder.
Then, I usually like to turn on all the --strictXXX compiler flags (I think you can just use --strict) to get the maximum number of warnings to ignore and/or fix.
Okay, now: the ES6/TypeScript idiom for a constructible thing is to use a class. Here's a look at some modifications I made, with some inline comments:
import * as moment from 'moment';
class Calendar {
// a Calendar has an el property which is a possibly null DOM element:
el: Element | null;
// a Calendar has a current property which is a Moment:
current: moment.Moment;
// a Calendar has an events property which is an array of Event:
events: Event[];
// the constructor function is what gets called when you do new Calendar()
// note that I assume selector is a string and events is an array of Event
constructor(selector: string, events: Event[]) {
this.el = document.querySelector(selector);
this.events = events;
this.current = moment().date(1);
this.draw();
var current = document.querySelector('.today');
if (current) {
var self = this;
window.setTimeout(function() {
self.openDay(current);
}, 500);
}
}
draw() {
// needs an implementation
}
openDay(day: Element | null) {
// needs an implementation
}
}
declare let data: Event[]; // need to define data
var calendar = new Calendar('#calendar', data);
You need to implement the draw() and openDay() methods which are presumably part of the Calendar.prototype. I put stubs for them in there. You also need to define data, which is (I'm guessing) an array of events (if it's something else you need to change the type of events.
If you look at the compiled JavaScript output from the above, you'll see that it's more or less the same as what you had. But now, of course, TypeScript is happy to let you call new Calendar(...).
There are more changes you can make, of course. For example, you can use parameter properties and remove the this.events = events; line. Or you can use property initializers and move the this.current = ... out of the constructor function and into the property declaration. Et cetera.
But this should hopefully be enough to get you started. Good luck!
I am very new to javascript.
Here I am failing to run an object method on a DOM element that I selected through another property of the same object. I suspect there is something wrong with my thinking!
Thanks in advance for any piece of help.
var Arrow = function() {
this.current = $('.arrow');
this.previous = null;
this.bend = function() {
// do bend
};
};
var arrow = new Arrow();
arrow.current.bend();
bend() is a method of Arrow, not current. Use arrow.bend() and it will also have access to current using this.current.
arrow.current.bend is not defined.
You have defined:
this.current as the Array of DOM elements.
this.bend as method with a function.
Hence, you can call:
arrow.current >> returns Array of DOMs
arrow.bend() >> executes function bend.
arrow.current.bend() does not exist.
Also, note that arrow.current is an array. You'd first need to get each of the elements:
for (element of arrow.current) { element.bend(); }
However, as said before, element does not have a bend element by default and you have not appended at any point. Only arrow has a bend property.
I hope this guides you on why this does not work.
However, if you want to open a question on what you are trying to achieve, maybe we can help to get it fixed.
You need to call bend() on arrow object. In bend() function, you do what you need to do.
var Arrow = function() {
this.current = $('.arrow');
this.previous = null;
this.bend = function() {
// do bend
current.style = 'bent';
};
};
var arrow = new Arrow();
arrow.bend();
So two things.
You called the right method on the wrong object
arrow.bend(); // not arrow.current.bend()
The second possible problem is with this.current = $('.arrow');. To get the an element from the DOM, you should make sure it's totally loaded. I'd suggest the following
var Arrow = function($arrow) {
this.current = $arrow;
this.previous = null;
};
// To avoid creating the `bend` in every instance of Arrow
Arrow.prototype.bend = function() {
console.log(this.current.attr('id'));
};
$(function () {
// Now it's certain that the DOM is completely loaded
var arrow = new Arrow($('.arrow').first());
arrow.bend();
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="arrow" id="toto">arrow<div>
I am having trouble understanding in what situations you would use custom events.
I mean the ones created by the CustomEvent constructor.
I understand the syntax itself, just not why it is useful. It would be nice if somebody could provide an example of real world application of custom events.
I use it (shameless plug) to raise "resize" events on div elements and then use a separate binding framework (aurelia) to listen to those events.
the explicit code example is:
var element = this.element; // some element
var erd = erd({ strategy: 'scroll' });
var widthOld = element.offsetWidth;
var heightOld = element.offsetHeight;
this.callback = () => {
var event = new CustomEvent("resize", {
detail: {
width: this.element.offsetWidth,
height: this.element.offsetHeight,
widthOld: widthOld,
heightOld: heightOld
}
});
element.dispatchEvent(event);
widthOld = this.element.offsetWidth;
heightOld = this.element.offsetHeight;
};
erd.listenTo(this.element, this.callback);
where erd is element-resize-detector that allows you to detect when any div changes shape.
In protractor I'm trying to clean up my locators, and organize things a little better. Currently, I have a variable that contains the element locator for a dialog, and a save button:
var StoryPage = function(){
this.dialog = element(by.css('md-dialog'));
this.saveButton = element(by.buttonText('Save'));
}
My question is, is there a way to chain these element variables, so that I can find the save button within the dialog like so:
this.dialog.saveButton.click()
or
this.dropdown.saveButton.click()
Thanks in advance!
Yes, you can chain the element finders in Protractor:
var StoryPage = function() {
this.dialog = element(by.css('md-dialog'));
this.saveButton = this.dialog.element(by.buttonText('Save'));
}
Now the Save button would be located within/in the scope/inside the md-dialog element.
If you want to "scale" that to multiple page objects, you can define a base page object:
var BasePage = function() {
this.getSaveButton = function () {
return this.dialog.element(by.buttonText('Save'));
}
}
module.exports = new BasePage();
Then, use prototypical inheritance to inherit your other page objects from the base one which would allow you have save buttons inside different dialog containers:
var BasePage = require("base");
var StoryPage = function(){
this.dialog = element(by.css('md-dialog'));
}
StoryPage.prototype = Object.create(BasePage);
module.exports = new StoryPage();
I edited the question so it would make more sense.
I have a function that needs a couple arguments - let's call it fc(). I am passing that function as an argument through other functions (lets call them fa() and fb()). Each of the functions that fc() passes through add an argument to fc(). How do I pass fc() to each function without having to pass fc()'s arguments separately? Below is how I want it to work.
function fa(fc){
fc.myvar=something
fb(fc)
}
function fb(fc){
fc.myothervar=something
fc()
}
function fc(){
doessomething with myvar and myothervar
}
Below is how I do it now. As I add arguments, it's getting confusing because I have to add them to preceding function(s) as well. fb() and fc() get used elsewhere and I am loosing some flexibility.
function fa(fc){
myvar=something
fb(fc,myvar)
}
function fb(fc,myvar){
myothervar=something
fc(myvar,myothervar)
}
function fc(myvar,myothervar){
doessomething with myvar and myothervar
}
Thanks for your help
Edit 3 - The code
I updated my code using JimmyP's solution. I'd be interested in Jason Bunting's non-hack solution. Remember that each of these functions are also called from other functions and events.
From the HTML page
<input type="text" class="right" dynamicSelect="../selectLists/otherchargetype.aspx,null,calcSalesTax"/>
Set event handlers when section is loaded
function setDynamicSelectElements(oSet) {
/**************************************************************************************
* Sets the event handlers for inputs with dynamic selects
**************************************************************************************/
if (oSet.dynamicSelect) {
var ySelectArgs = oSet.dynamicSelect.split(',');
with (oSet) {
onkeyup = function() { findListItem(this); };
onclick = function() { selectList(ySelectArgs[0], ySelectArgs[1], ySelectArgs[2]) }
}
}
}
onclick event builds list
function selectList(sListName, sQuery, fnFollowing) {
/**************************************************************************************
* Build a dynamic select list and set each of the events for the table elements
**************************************************************************************/
if (fnFollowing) {
fnFollowing = eval(fnFollowing)//sent text function name, eval to a function
configureSelectList.clickEvent = fnFollowing
}
var oDiv = setDiv(sListName, sQuery, 'dynamicSelect', configureSelectList); //create the div in the right place
var oSelected = event.srcElement;
if (oSelected.value) findListItem(oSelected)//highlight the selected item
}
Create the list
function setDiv(sPageName, sQuery, sClassName, fnBeforeAppend) {
/**************************************************************************************
* Creates a div and places a page in it.
**************************************************************************************/
var oSelected = event.srcElement;
var sCursor = oSelected.style.cursor; //remember this for later
var coords = getElementCoords(oSelected);
var iBorder = makeNumeric(getStyle(oSelected, 'border-width'))
var oParent = oSelected.parentNode
if (!oParent.id) oParent.id = sAutoGenIdPrefix + randomNumber()//create an ID
var oDiv = document.getElementById(oParent.id + sWindowIdSuffix)//see if the div already exists
if (!oDiv) {//if not create it and set an id we can use to find it later
oDiv = document.createElement('DIV')
oDiv.id = oParent.id + sWindowIdSuffix//give the child an id so we can reference it later
oSelected.style.cursor = 'wait'//until the thing is loaded
oDiv.className = sClassName
oDiv.style.pixelLeft = coords.x + (iBorder * 2)
oDiv.style.pixelTop = (coords.y + coords.h + (iBorder * 2))
XmlHttpPage(sPageName, oDiv, sQuery)
if (fnBeforeAppend) {
fnBeforeAppend(oDiv)
}
oParent.appendChild(oDiv)
oSelected.style.cursor = ''//until the thing is loaded//once it's loaded, set the cursor back
oDiv.style.cursor = ''
}
return oDiv;
}
Position and size the list
function configureSelectList(oDiv, fnOnClick) {
/**************************************************************************************
* Build a dynamic select list and set each of the events for the table elements
* Created in one place and moved to another so that sizing based on the cell width can
* occur without being affected by stylesheet cascades
**************************************************************************************/
if(!fnOnClick) fnOnClick=configureSelectList.clickEvent
if (!oDiv) oDiv = configureSelectList.Container;
var oTable = getDecendant('TABLE', oDiv)
document.getElementsByTagName('TABLE')[0].rows[0].cells[0].appendChild(oDiv)//append to the doc so we are style free, then move it later
if (oTable) {
for (iRow = 0; iRow < oTable.rows.length; iRow++) {
var oRow = oTable.rows[iRow]
oRow.onmouseover = function() { highlightSelection(this) };
oRow.onmouseout = function() { highlightSelection(this) };
oRow.style.cursor = 'hand';
oRow.onclick = function() { closeSelectList(0); fnOnClick ? fnOnClick() : null };
oRow.cells[0].style.whiteSpace = 'nowrap'
}
} else {
//show some kind of error
}
oDiv.style.width = (oTable.offsetWidth + 20) + "px"; //no horiz scroll bars please
oTable.mouseout = function() { closeSelectList(500) };
if (oDiv.firstChild.offsetHeight < oDiv.offsetHeight) oDiv.style.height = oDiv.firstChild.offsetHeight//make sure the list is not too big for a few of items
}
Okay, so - where to start? :) Here is the partial function to begin with, you will need this (now and in the future, if you spend a lot of time hacking JavaScript):
function partial(func /*, 0..n args */) {
var args = Array.prototype.slice.call(arguments, 1);
return function() {
var allArguments = args.concat(Array.prototype.slice.call(arguments));
return func.apply(this, allArguments);
};
}
I see a lot of things about your code that make me cringe, but since I don't have time to really critique it, and you didn't ask for it, I will suggest the following if you want to rid yourself of the hack you are currently using, and a few other things:
The setDynamicSelectElements() function
In this function, you can change this line:
onclick = function() { selectList(ySelectArgs[0], ySelectArgs[1], ySelectArgs[2]) }
To this:
onclick = function() { selectList.apply(null, ySelectArgs); }
The selectList() function
In this function, you can get rid of this code where you are using eval - don't ever use eval unless you have a good reason to do so, it is very risky (go read up on it):
if (fnFollowing) {
fnFollowing = eval(fnFollowing)
configureSelectList.clickEvent = fnFollowing
}
And use this instead:
if(fnFollowing) {
fnFollowing = window[fnFollowing]; //this will find the function in the global scope
}
Then, change this line:
var oDiv = setDiv(sListName, sQuery, 'dynamicSelect', configureSelectList);
To this:
var oDiv = setDiv(sListName, sQuery, 'dynamicSelect', partial(configureSelectListAlternate, fnFollowing));
Now, in that code I provided, I have "configureSelectListAlternate" - that is a function that is the same as "configureSelectList" but has the parameters in the reverse order - if you can reverse the order of the parameters to "configureSelectList" instead, do that, otherwise here is my version:
function configureSelectListAlternate(fnOnClick, oDiv) {
configureSelectList(oDiv, fnOnClick);
}
The configureSelectList() function
In this function, you can eliminate this line:
if(!fnOnClick) fnOnClick=configureSelectList.clickEvent
That isn't needed any longer. Now, I see something I don't understand:
if (!oDiv) oDiv = configureSelectList.Container;
I didn't see you hook that Container property on in any of the other code. Unless you need this line, you should be able to get rid of it.
The setDiv() function can stay the same.
Not too exciting, but you get the idea - your code really could use some cleanup - are you avoiding the use of a library like jQuery or MochiKit for a good reason? It would make your life a lot easier...
A function's properties are not available as variables in the local scope. You must access them as properties. So, within 'fc' you could access 'myvar' in one of two ways:
// #1
arguments.callee.myvar;
// #2
fc.myvar;
Either's fine...
Try inheritance - by passing your whatever object as an argument, you gain access to whatever variables inside, like:
function Obj (iString) { // Base object
this.string = iString;
}
var myObj = new Obj ("text");
function InheritedObj (objInstance) { // Object with Obj vars
this.subObj = objInstance;
}
var myInheritedObj = new InheritedObj (myObj);
var myVar = myInheritedObj.subObj.string;
document.write (myVar);
subObj will take the form of myObj, so you can access the variables inside.
Maybe you are looking for Partial Function Application, or possibly currying?
Here is a quote from a blog post on the difference:
Where partial application takes a function and from it builds a function which takes fewer arguments, currying builds functions which take multiple arguments by composition of functions which each take a single argument.
If possible, it would help us help you if you could simplify your example and/or provide actual JS code instead of pseudocode.