I'm attempting to build a basic JS plugin that can be called after a click event to disable a button (to prevent users firing multiple API calls) and to give feedback that something is loading/happening. Here is how it looks:
This works great on an individual basis, but I want to re-write it as a plugin so I can reuse it across the site.
Here is a cut down version of the JS from file loader.plugin.js.
let originalBtnText;
export function showBtnLoader(btn, loadingText) {
const clickedBtn = btn;
const spinner = document.createElement('div');
spinner.classList.add('spin-loader');
originalBtnText = clickedBtn.textContent;
clickedBtn.textContent = loadingText;
clickedBtn.appendChild(spinner);
clickedBtn.setAttribute('disabled', true);
clickedBtn.classList.add('loading');
return this;
}
export function hideBtnLoader(btn) {
const clickedBtn = btn.target;
clickedBtn.textContent = originalBtnText;
clickedBtn.removeAttribute('disabled');
clickedBtn.classList.remove('loading');
return this;
}
export function btnLoader() {
showBtnLoader();
hideBtnLoader();
}
And here is an example of how I would like to use it.
import btnLoader from 'loaderPlugin';
const signupBtn = document.getElementById('signup-btn');
signupBtn.addEventListener('click', function(e) {
e.preventDefault();
btnLoader.showBtnLoader(signupBtn, 'Validating');
// Call API here
});
// Following API response
hideBtnLoader(signupBtn);
The issue I have is that I want to store the originalBtnText from the showBtnLoader function and then use that variable in the hideBtnLoader function. I could of course achieve this in a different way (such as adding the value as a data attribute and grabbing it later) but I wondered if there is a simple way.
Another issue I have is that I don't know the correct way of calling each individual function and whether I am importing it correctly. I have tried the following.
btnLoader.showBtnLoader(signupBtn, 'Validating');
btnLoader(showBtnLoader(signupBtn, 'Validating'));
showBtnLoader(signupBtn, 'Validating');
But I get the following error:
Uncaught ReferenceError: showBtnLoader is not defined
at HTMLButtonElement.<anonymous>
I have read some good articles and SO answers such as http://2ality.com/2014/09/es6-modules-final.html and ES6 export default with multiple functions referring to each other but I'm slightly confused as to the 'correct' way of doing this to make it reusable.
Any pointers would be much appreciated.
I would export a function that creates an object with both show and hide functions, like this:
export default function(btn, loadingText) {
function show() {
const clickedBtn = btn;
const spinner = document.createElement('div');
spinner.classList.add('spin-loader');
originalBtnText = clickedBtn.textContent;
clickedBtn.textContent = loadingText;
clickedBtn.appendChild(spinner);
clickedBtn.setAttribute('disabled', true);
clickedBtn.classList.add('loading');
}
function hide() {
const clickedBtn = btn.target;
clickedBtn.textContent = originalBtnText;
clickedBtn.removeAttribute('disabled');
clickedBtn.classList.remove('loading');
}
return {
show,
hide,
};
}
Then, to use it:
import btnLoader from 'btnloader';
const signupBtn = document.getElementById('signup-btn');
const signupLoader = btnLoader( signupBtn, 'Validating' );
signupBtn.addEventListener('click', function(e) {
e.preventDefault();
signupLoader.show();
// Call API here
});
// Following API response
signupLoader.hide();
If you need to hide it from a different file from where you showed it, then you can export the instance:
export const signupLoader = btnLoader( signupBtn, 'Validating' );
And later import it.
import { signupLoader } from 'signupform';
function handleApi() {
signupLoader.hide();
}
Youre maybe overriding the Element.prototype, to make it accessible right from that element. However, i wouldnt set values onto that element, i would rather return an object with all the neccessary stuff:
export function implementBtnLoader(){
Element.prototype.showBtnLoader=function( loadingText) {
const clickedBtn = this;
const spinner = document.createElement('div');
spinner.classList.add('spin-loader');
var originalBtnText = clickedBtn.textContent;
clickedBtn.textContent = loadingText;
clickedBtn.appendChild(spinner);
clickedBtn.setAttribute('disabled', true);
clickedBtn.classList.add('loading');
return {
text:originalBtnText,
el:this,
hideBtnLoader: function() {
const clickedBtn = this.target;
clickedBtn.textContent = this.text;
clickedBtn.removeAttribute('disabled');
clickedBtn.classList.remove('loading');
return this;
}
};
};
}
export function btnLoader() {
implementBtnLoader();
}
When imported, and implementBtnLoader was called, one can do:
var loader=document.getElementById("test").showBtnLoader();
console.log(loader.text);
loader.hideBtnLoader();
Related
I wanted to know if it was possible by combining webpack and js' oop to arrive at a functional code like the one presented below.
The goal is to be able to isolate each of the elements of my site (sidebar, main,...) in different files while making sure that they can interact between.
Is this possible with webpack and pure js or not?
import ApplePicker from "./my_path/applePicker.js";
import NiceFarmer from "./my_path/niceFarmer.js";
const orchard = function () {
const appleNumber = 10;
const jack = new ApplePicker();
const daniel = new NiceFarmer();
jack.eatAnApple();
daniel.eatAnApple();
// appleNumber have to be now === 2
}
// Example of applePicker.js structure
const ApplePicker = function () {
this.eatAnApple = function () {
// Do something
}
}
export default ApplePicker;
yes you can.
If you simply want to consume a variable from applePicker.js:
// applePicker.js
export const apples = 10
If you want to be able to reassign that variable you might want to add a simple facade layer on top of that:
// applePicker.js
let apple = 5
export function getApple() {
return apple;
}
export function setApple(newValue) {
apple = newValue;
}
Even better, if you want other functions to "fire" when a variable is changed, use the Observer pattern
I have integrated some HTML/JS Code into my Vaadin WebApplication by creating an AbstractJavaScriptComponent. The Component almost works as intended.
How do I call the passInfo() method defined in the "connector.js" without having to manually click the Button defined in the innerHTML of the "chessControll.JsLabel" Component in "chessControll.js". My Goal is to pass Information, when the onChange Event of the init() function is called, which is located in the same file "chessControll.js", but not part of the Component.
I have already tried to create a Custom Event and then dispatch it whenever onChange() in the init() function is called, it worked as long as I didn't listen for the Event inside of my Component (chessControll.js, chessControll.JsLabel). It seems it can only be accessed in a static way.
How can I access the chessControll.JsLabel in "chessControll.js" from the init() function and then dispatch the button click or listen for events inside the component to achieve the same?
connector.js:
com_*myname*_*applicationName*_JsLabel = function() {
var mycomponent = new chessControll.JsLabel(this.getElement());
connector = this;
this.onStateChange = function() {
mycomponent = this.getState().boolState;
};
mycomponent.click = function() {
connector.passInfo(true);
};
};
chessControll.js:
var chessControll = chessControll || {};
chessControll.JsLabel = function (element) {
element.innerHTML =
"<input type='button' value='Click'/>";
// Getter and setter for the value property
this.getValue = function () {
return element.
getElementsByTagName("input")[0].value;
};
var button = element.getElementsByTagName("input")[0];
var self = this;
button.onclick = function () {
self.click();
};
};
var init = function() {
var onChange = function() {
/*Click Button defined in JsLabel Component */
};
};$(document).ready(init);
I figured out what the problem was.
The architecture of Java Web Applications doesn't allow a simple communication like i did in my example. The JavaScript made a call from the Client Side to the Server Side Vaadin Component.
I integrated the whole JavaScript, including the init function, as a Component. This way i can call the method from the init function because everything is known on the Server Side.
edited chessControll.js :
var chessControll = chessControll || {};
chessControll.JsLabel = function (element) {
element.innerHTML =
"<input type='button' value='Click'/>";
// Getter and setter for the value property
this.getValue = function () {
return element.
getElementsByTagName("input")[0].value;
};
var button = element.getElementsByTagName("input")[0];
var self = this;
//deleted bracket here
var init = function() {
var onChange = function() {
self.click();
};
};$(document).ready(init);
} //<-- that Simple
My object is a independent js file that I created.
componentDidMount() {
const node = ReactDOM.findDOMNode(this);
const widgetBuild = new window.WidgetFormBuilder({
form: $(node).parents('#dynamic_form_wrapper')
});
widgetBuild.initForm();
}
It is a little hard to workout without knowing more about WidgetFormBuilder.
However as good practice I would suggest...
componentDidMount() {
const node = ReactDOM.findDOMNode(this);
// Assign to the class instance
this.widgetBuild = new window.WidgetFormBuilder({
form: $(node).parents('#dynamic_form_wrapper')
});
this.widgetBuild.initForm();
}
componentWillUnmount() {
// Cleanup
// Check if WidgetFormBuilder has a destroy method or something similar.
// See https://reactjs.org/docs/react-component.html#componentwillunmount
this.widgetBuild = null;
}
shouldComponentUpdate() {
// Stop further re-renders, given you're using the DOM directly this could help prevent a few performance issues
// See https://reactjs.org/docs/react-component.html#shouldcomponentupdate
return false;
}
Finally, take a look at the react docs on third party libs.
I already fixed i just added a .destroy() function in my WidgetFormBuilder. :)
WidgetFormBuilder.prototype.destroyBuilder = function () {
const self = this;
const destroyEvents = function () {
$(self.form).unbind();
};
destroyEvents();
return this;
};
I am trying to creat to page object files with protractor.
My app has the following layout.
and page object files..
navbar_po.js
var NavBar = function() {
// define navbar elements and operations
// .
// .
};
module.exports = NavBar;
subNavbar_po.js
var SubNavBar = function() {
// define subnavbar elements and operations
// .
// .
};
module.exports = SubNavBar;
page1_po.js
var Page1 = function() {
this.navbar = function(){
var navbar = require('./navbar_po.js');
return new navbar();
}
this.subnavbar = function(){
var subnavbar = require('./subNavbar_po.js');
return new subnavbar();
}
// define Page1 particular elements and operations
// .
// .
};
module.exports = Page1;
and I access the navbar elements as follows in test script..
var page1 = new require('./page1_po.js');
page1.navbar.something_method();
page1.subnavbar.something_method();
Is this the best way?
I don't want to define the same navbar elements for each page object file.
Is there any other good way?
Ok, great question, super detailed information too!
🛠Let's get to it. We'll be using ES6 syntax!
Small structural conventions will make the project easier to maintain and won't need defining the same navbar (or any other) elements for each page object file.
Also, the suggested filename kebab-case style will help keep things recognizable as your project grows :)
navbar_po.js // suggest renaming to nav-bar.pageObject.js
export default class NavBar {
constructor() {
this.homePageButton = element(by.id('home-button'))
// more element locators
}
function clickHomePageButton() {
this.homePageButton.click()
}
// more operations
}
subNavbar_po.js // suggest renaming to sub-nav-bar.pageObject.js
export default class SubNavBar {
constructor() {
this.aboutPageButton = element(by.id('about-button'))
// more element locators
}
function clickAboutPageButton() {
this.aboutPageButton.click()
}
// more operations
}
page1_po.js // suggest renaming to page-one.pageObject.js
import SubNavBar from './sub-nav-bar.pageObject' // the .js at the end can be omitted, it will know it's JS!
import navBar from './nav-bar.pageObject'
export default class pageOne {
constructor() {
this.NavBar = NavBar
this.SubNavBar = SubNavBar
// more element locators
}
function visitHomePageThenAboutPage() {
const navBar = new NavBar()
const subNavBar = new SubNavBar
navBar.clickHomePageButton()
subNavBar.clickAboutPageButton()
}
// more operations
}
your test script
pageOne.spec.js
If any of your elements change, you only ever need to change them in the relevant pageObject file. Nothing else needs changing, not even your test script!
import PageOne from './page-one.pageObject');
describe('navigating', () => {
it('should go to home and about', () => {
const pageOne = new PageOne()
pageOne.visitHomePageThenAboutPage()
// expect statement
}
}
💡 pageObject concept
Imagine that your test script mimics human interaction with your site. And pageObjects are how you abstract away and hide exactly what needs to be done for those user behaviours to interact with your page.
📚 Resources
The following is a great styleguide that helps keep things manageable ✨
https://github.com/CarmenPopoviciu/protractor-styleguide
Also, checkout this (disclaimer: my) curated list on all things awesome for Protractor! Make a pull request if you ever want to add anything!
Good luck testing!
https://github.com/chowdhurian/awesome-protractor
Maybe write a utility for that:
// navbarutil.js
var NavbarUtil = {
create: function(page) {
page.navbar = function() {
return new require('./navbar_po.js')();
}
page.subnavbar = function() {
return new require('./subNavbar_po.js')();
}
}
};
module.exports = NavbarUtil;
And in your page use:
var Page1 = function() {
require('./navbarutil.js').create(this);
// Define other things here...
}
module.exports = Page1;
We are using the Page Object pattern to organize our internal AngularJS application tests.
Here is an example page object we have:
var LoginPage = function () {
this.username = element(by.id("username"));
this.password = element(by.id("password"));
this.loginButton = element(by.id("submit"));
}
module.exports = LoginPage;
In a single-browser test, it is quite clear how to use it:
var LoginPage = require("./../po/login.po.js");
describe("Login functionality", function () {
var scope = {};
beforeEach(function () {
browser.get("/#login");
scope.page = new LoginPage();
});
it("should successfully log in a user", function () {
scope.page.username.clear();
scope.page.username.sendKeys(login);
scope.page.password.sendKeys(password);
scope.page.loginButton.click();
// assert we are logged in
});
});
But, when it comes to a test when multiple browsers are instantiated and there is the need to switch between them in a single test, it is becoming unclear how to use the same page object with multiple browsers:
describe("Login functionality", function () {
var scope = {};
beforeEach(function () {
browser.get("/#login");
scope.page = new LoginPage();
});
it("should warn there is an opened session", function () {
scope.page.username.clear();
scope.page.username.sendKeys(login);
scope.page.password.sendKeys(password);
scope.page.loginButton.click();
// assert we are logged in
// fire up a different browser and log in
var browser2 = browser.forkNewDriverInstance();
// the problem is here - scope.page.username.clear() would be applied to the main "browser"
});
});
Problem:
After we forked a new browser, how can we use the same Page Object fields and functions, but applied to a newly instantiated browser (browser2 in this case)?
In other words, all element() calls here would be applied to browser, but needed to be applied to browser2. How can we switch the context?
Thoughts:
one possible approach here would be to redefine the global element = browser2.element temporarily while being in the context of browser2. The problem with this approach is that we also have browser.wait() calls inside the page object functions. This means that browser = browser2 should be also set. In this case, we would need to remember the browser global object in a temp variable and restore it once we switch back to the main browser context..
another possible approach would be to pass the browser instance into the page object, something like:
var LoginPage = function (browserInstance) {
browser = browserInstance ? browserInstance : browser;
var element = browser.element;
// ...
}
but this would probably require to change every page object we have..
Hope the question is clear - let me know if it needs clarification.
Maybe you could write few functions to make the the browser registration/start/switch smoother. (Basically it is your first option with some support.)
For example:
var browserRegistry = [];
function openNewBrowser(){
if(typeof browserRegistry[0] == 'undefined'){
browseRegistry[0] = {
browser: browser,
element: element,
$: $,
$$: $$,
... whatever else you need.
}
}
var tmp = browser.forkNewDriverInstance();
var id = browserRegistry.length;
browseRegistry[id] = {
browser: tmp,
element: tmp.element,
$: tmp.$,
$$: tmp.$$,
... whatever else you need.
}
switchToBrowserContext(id);
return id;
}
function switchToBrowserContext(id){
browser=browseRegistry[id].browser;
element=browseRegistry[id].element;
$=browseRegistry[id].$;
$$=browseRegistry[id].$$;
}
And you use it this way in your example:
describe("Login functionality", function () {
var scope = {};
beforeEach(function () {
browser.get("/#login");
scope.page1 = new LoginPage();
openNewBrowser();
browser.get("/#login");
scope.page2 = new LoginPage();
});
it("should warn there is an opened session", function () {
scope.page1.username.clear();
scope.page1.username.sendKeys(login);
scope.page1.password.sendKeys(password);
scope.page1.loginButton.click();
scope.page2.username.clear();
scope.page2.username.sendKeys(login);
scope.page2.password.sendKeys(password);
scope.page2.loginButton.click();
});
});
So you can leave your page objects as they are.
To be honest I think your second approach is cleaner...
Using global variables can bite back later.
But if you don't want to change your POs, this can also work.
(I did not test it... sorry for the likely typos/errors.)
(You can place the support functions to your protractor conf's onprepare section for example.)
Look at my solution. I simplified example, but we are using this approach in current project. My app has pages for both user permissions types, and i need to do some complex actions same time in both browsers. I hope this might show you some new, better way!
"use strict";
//In config, you should declare global browser roles. I only have 2 roles - so i make 2 global instances
//Somewhere in onPrepare() function
global.admin = browser;
admin.admin = true;
global.guest = browser.forkNewDriverInstance();
guest.guest = true;
//Notice that default browser will be 'admin' example:
// let someElement = $('someElement'); // this will be tried to be found in admin browser.
class BasePage {
//Other shared logic also can be added here.
constructor (browser = admin) {
//Simplified example
this._browser = browser
}
}
class HomePage extends BasePage {
//You will not directly create this object. Instead you should use .getPageFor(browser)
constructor(browser) {
super(browser);
this.rightToolbar = ToolbarFragment.getFragmentFor(this._browser);
this.chat = ChatFragment.getFragmentFor(this._browser);
this.someOtherNiceButton = this._browser.$('button.menu');
}
//This function relies on params that we have patched for browser instances in onPrepare();
static getPageFor(browser) {
if (browser.guest) return new GuestHomePage(browser);
else if (browser.admin) return new AdminHomePage(browser);
}
openProfileMenu() {
let menu = ProfileMenuFragment.getFragmentFor(this._browser);
this.someOtherNiceButton.click();
return menu;
}
}
class GuestHomePage extends RoomPage {
constructor(browser) {
super(browser);
}
//Some feature that is only available for guest
login() {
// will be 'guest' browser in this case.
this._browser.$('input.login').sendKeys('sdkfj'); //blabla
this._browser.$('input.pass').sendKeys('2345'); //blabla
this._browser.$('button.login').click();
}
}
class AdminHomePage extends RoomPage {
constructor(browser) {
super(browser);
}
acceptGuest() {
let acceptGuestButton = this._browser.$('.request-admission .control-btn.admit-user');
this._browser.wait(EC.elementToBeClickable(acceptGuestButton), 10000,
'Admin should be able to see and click accept guest button. ' +
'Make sure that guest is currently trying to connect to the page');
acceptGuestButton.click();
//Calling browser directly since we need to do complex action. Just example.
guest.wait(EC.visibilityOf(guest.$('.central-content')), 10000, 'Guest should be dropped to the page');
}
}
//Then in your tests
let guestHomePage = HomePage.getPageFor(guest);
guestHomePage.login();
let adminHomePage = HomePage.getPageFor(admin);
adminHomePage.acceptGuest();
adminHomePage.openProfileMenu();
guestHomePage.openProfileMenu();