how should i dynamically update a firebase document - javascript

i'm working on a simple note-taking app for my portfolio using JS and Firebase. Before i tell you what's happening i feel like i need to show you how my code works, if you have any tips and concerns please tell me as it would be GREATLY appreciated. That being said, let's have a look "together". I'm using this class to create the notes:
const htmlElements = [document.querySelector('.notes'), document.querySelector('.note')];
const [notesDiv, noteDiv] = htmlElements;
class CreateNote {
constructor(title, body) {
this.title = title;
this.body = body;
this.render = () => {
const div1 = document.createElement('div');
div1.className = 'notes-prev-container';
div1.addEventListener('click', () => { this.clickHandler(this) });
const div2 = document.createElement('div');
div2.className = 'notes-prev';
const hr = document.createElement('hr');
hr.className = 'notes__line';
// Nest 'div2' inside 'div1'
div1.appendChild(div2);
div1.appendChild(hr);
/*
Create Paragraph 1 & 2 and give them the same
class name and some text
*/
const p1 = document.createElement('p');
p1.className = 'notes-prev__title';
p1.innerText = this.title;
const p2 = document.createElement('p');
p2.className = 'notes-prev__body';
p2.innerText = this.body;
// Nest p 1 & 2 inside 'div2'
div2.appendChild(p1);
div2.appendChild(p2);
// Finally, render the div to its root tag
notesDiv.appendChild(div1);
}
}
/*
Every time this method is called, it creates 2 textareas,
one for the note title and the other for its body then it
appends it to the DOM.
*/
renderNoteContent () {
const title = document.createElement('textarea');
title.placeholder = 'Title';
title.value = this.title;
title.className = 'note__title';
const body = document.createElement('textarea');
body.placeholder = 'Body';
body.value = this.body;
body.className = 'note__body';
noteDiv.appendChild(title);
noteDiv.appendChild(body);
}
/*
When this method is called, it checks to see if there's a
note rendered already (childElementCount === 1 because there's a
button, so if there's only this button it means there's no
textareas rendered).
If yes, then merely call the renderNoteContent method. Else
get the tags with the classes 'note__title' and 'note__body'
and remove them from the DOM, then call renderNoteContent to
create the textareas with the clicked notes values.
This function gets mentioned at line 19.
*/
clickHandler(thisClass) {
if (noteDiv.childElementCount === 1) {
thisClass.renderNoteContent();
} else {
document.querySelector('.note__title').remove();
document.querySelector('.note__body').remove();
thisClass.renderNoteContent();
}
}
}
Now i need 2 buttons, createNotesButton and saveNotesButton respectively. These 2 buttons must be inside a function that will be called inside .onAuthStateChanged (why? because they will be needing access to the currentUser on firebase auth).
I want the createNotesButton to create a note prototype, render it to the DOM and create a new document on firestore, where this note contents will be stored. Here's how i did it:
PS: I feel like i'm not using this class correctly, so again if you have any tips i appreciate it.
import {db} from '../../firebase_variables/firebase-variables.js';
import {CreateNote} from '../create_notes_class/create_notes_class.js';
const htmlElements = [
document.querySelector('.createNotes-button'),
document.querySelector('.saveNotes-button')
];
const [createNotesButton, saveNotesButton] = htmlElements;
function clickHandler(user) {
/*
1. Creates a class.
2. Creates a new document on firebase with the class's empty value.
3. Renders the empty class to the DOM.
*/
createNotesButton.addEventListener('click', () => {
const note = new CreateNote('', '');
note.render();
// Each user has it's own note collection, said collection has their `uid` as name.
db.collection(`${user.uid}`).doc().set({
title: `${note.title}`,
body: `${note.body}`
})
})
}
Now i need a saveNotesButton, he's the one i'm having issues with. He needs to save the displayed note's content on firestore. Here's what i tried doing:
import {db} from '../../firebase_variables/firebase-variables.js';
import {CreateNote} from '../create_notes_class/create_notes_class.js';
const htmlElements = [
document.querySelector('.createNotes-button'),
document.querySelector('.saveNotes-button')
];
const [createNotesButton, saveNotesButton] = htmlElements;
function clickHandler(user) {
createNotesButton.addEventListener('click', () => {...})
/*
1. Creates 2 variables, `title` and `body, if there's not a note being displayed
their values will be null, which is why the rest of the code is inside an if
statement
2. If statement to check if there's a note being displayed, if yes then:
1. Call the user's note collection. Any document who has the title field equal to the
displayed note's value gets returned as a promise.
2. Then call an specific user document and update the fields `title` and `body` with
the displayed note's values.
3. If no then do nothing.
*/
saveNotesButton.addEventListener('click', () => {
const title = document.querySelector('.note__title');
const body = document.querySelector('.note__body');
db.collection(`${user.uid}`).where('title', '==', `${title.value}`)
.get()
.then(userCollection => {
db.collection(`${user.uid}`).doc(`${userCollection.docs[0].id}`).update({
title: `${title.value}`,
body: `${body.value}`
})
})
.catch(error => {
console.log('Error getting documents: ', error);
});
});
}
This didn't work because i'm using title.value as a query, so if i change it's value it will also change the queries direction to a path that doesn't exist.
So here's the question: how can i make it so the saveNotesButton does its job? I was thinking of adding another field to each note, something that won't change so i can easily identify and edit each note. Again, if there's something in my code that you think can or should be formatted please let me know, i'm using this project as a way to solidify my native JS knowledge so please be patient. I feel like if i had used React i would've finished this sometime ago but definitely wouldn't have learned as much, anyway thanks for your help in advance.

I was thinking of adding another field to each note, something that won't change so i can easily identify and edit each note.
Yes, you absolutely need an immutable identifier for each note document in the firestore so you can unambiguously reference it. You almost always want this whenever you're storing a data object, in any application with any database.
But, the firestore already does this for you: after calling db.collection(user.uid).doc() you should get a doc with an ID. That's the ID you want to use when updating the note.
The part of your code that interacts with the DOM will need to keep track of this. I suggest moving the code the creates the firestore document into the constructor of CreateNote and storing it on this. You'll need the user id there as well.
constructor(title, body, userId) {
this.title = title;
this.body = body;
const docRef = db.collection(userId).doc();
this.docId = docRef.id;
/* etc. */
Then any time you have an instance of CreateNote, you'll know the right user and document to reference.
Other suggestions (since you asked)
Use JsPrettier. It's worth the setup, you'll never go back.
Use HTML semantics correctly. Divs shouldn't be appended as children of hrs, because they're for "a thematic break between paragraph-level elements: for example, a change of scene in a story, or a shift of topic within a section." MDN
For your next project, use a framework. Essentially no one hand-codes event listeners and appends children to get things done. I see the value for basic understanding, but there's a rich and beautiful world of frameworks out there; don't limit yourself by avoiding them :-)

Related

Creating a If/Else Statement in JS which is dependant on a data attribute

So I have an app, which contains a button directing people to my 'Ccenter', normally this would bring me to a .DE domain, but I want to create the ability for it to redirect to the .AT instance of my site.
I can decide which domain the button should bring me to by looking at my recipient data, which always contains a country specific email address.
'recipient_email':data.ticket.recipient,
I thought the best way for this to work is an If/Else Statement on the Button itself, where I could use if “data.ticket.recipient.contains?(.de)” to ensure the button links to the .DE app instance, otherwise link to the .AT instance.
I'm unsure how to structure that statement within my code, and would appreciate some pointers or advice on if this is the best way to achieve what I'm aiming for.
Current code:
client.get(`ticket.customField:${cCenterCaseIdFieldName}`).then((result) => {
updateDataPoints(result, data, cCenterCaseIdFieldName);
const recipientEmail = data.ticket.recipient;
const zendeskID = data.ticket.id;
const cCenterUrlAustria = getAustrianCcenterUrl(zendeskID)
const cCenterUrl = getCcenterUrl(zendeskID);
const collapse = $('#collapseExample')
$("#myBtnToCcenter").click(() => openModalPopup(cCenterUrl));
$("#myBtnToAustrianCcenter").click(() => openModalPopup(cCenterUrlAustria));
Modal Pop Up Which Results From Button Click:
function openModalPopup(locationUrl) {
const modalOptions = {
location: 'modal',
url: locationUrl,
size: {
width: '80vw',
height: '80vh'
}
};
i guess you could just do sth. like this:
const recipientEmail = data.ticket.recipient;
var cCenterUrl;
if(recipientEmail.indexOf(".de") > 0) {
cCenterUrl = getCcenterUrl(zendeskID);
}else{
cCenterUrl = getAustrianCcenterUrl(zendeskID)
}
$("#myBtnToCcenter").click(() => openModalPopup(cCenterUrl));
and just use 1 button, if cCenterUrl can change during runtime, u cannot use const of course
One way would be to get the correct URL first, then set the onClick handler accordingly. Something like:
var url = data.ticket.recipient.includes(".de") ? getCcenterUrl(zendeskID) : getAustrianCcenterUrl(zendeskID);
$("#myBtnToCcenter").click(() => openModalPopup(url));
Side note: The includes() function is case-sensitive. If you are not sure of the case (upper/lowercase) of the data.ticket.recipient string, then you can do
data.ticket.recipient.toLowerCase().includes(".de".toLowerCase())
instead of data.ticket.recipient.includes(".de").

Sort Descending for Javascript cards

I am trying to create a sorting method that sorts the cards in the DOM from Z to A when I press a button. So far, I have created the logic to sort the array correctly but can't seem to render it out.
I have a Drink Class and a DrinkCard Class, and the DrinkCard does the actual card creation, and the Drink creates the Drink.
I feel like calling the Drink class would help render sorted array to the DOM, but not sure how I would do that. Drawing blanks.
This is what I have so far
UPDATE I updated with the suggestion below, but I don't have a rendered-content id anywhere. So, I used the querySelector on the class .card and this is the current error.
Uncaught DOMException: Failed to execute 'appendChild' on 'Node': The new child element contains the parent.
at Drink.render (file:///Users/austinredmond/dev/caffeine_me/frontend/src/models/drink.js:28:17)
at file:///Users/austinredmond/dev/caffeine_me/frontend/src/index.js:43:38
at Array.forEach (<anonymous>)
at HTMLInputElement.<anonymous> (file:///Users/austinredmond/dev/caffeine_me/frontend/src/index.js:43:17)
render # drink.js:28
(anonymous) # index.js:43
(anonymous) # index.js:43
sortDesc.addEventListener("click", () => {
const sortedArray = allDrinks.sort((a, b) => {
const nameA = a.name.toLowerCase(),
nameB = b.name.toLowerCase()
if (nameA < nameB) //sort string ascending
return 1
if (nameA > nameB)
return -1
return 0 //default return value (no sorting)
})
const node = document.querySelector('.card');
sortedArray.forEach(card => card.render(node));
})
Drink Class
class Drink {
constructor(data) {
// Assign Attributes //
this.id = data.id
this.name = data.name
this.caffeine = data.caffeine
this.comments = []
this.card = new DrinkCard(this, this.comments)
}
// Searches allDrinks Array and finds drink by id //
static findById(id) {
return allDrinks.find(drink => drink.id === id)
}
// Delete function to Delete from API //
delete = () => {
api.deleteDrink(this.id)
delete this
}
render(element) {
// this method will render each card; el is a reference to a DOM node
console.log(element)
element.appendChild(this.card.cardContent);
}
}
DrinkCard Class
class DrinkCard {
constructor(drink, comments) {
// Create Card //
const card = document.createElement('div')
card.setAttribute("class", "card w-50")
main.append(card)
card.className = 'card'
// Add Nameplate //
const drinkTag = document.createElement('h3')
drinkTag.innerText = drink.name
card.append(drinkTag)
// Add CaffeinePlate //
const caffeineTag = document.createElement('p')
caffeineTag.innerText = `Caffeine Amount - ${drink.caffeine}`
card.append(caffeineTag)
// Adds Create Comment Input Field //
const commentInput = document.createElement("input");
commentInput.setAttribute("type", "text");
commentInput.setAttribute("class", "input-group mb-3")
commentInput.setAttribute("id", `commentInput-${drink.id}`)
commentInput.setAttribute("placeholder", "Enter A Comment")
card.append(commentInput);
// Adds Create Comment Button //
const addCommentButton = document.createElement('button')
addCommentButton.innerText = "Add Comment"
addCommentButton.setAttribute("class", "btn btn-primary btn-sm")
card.append(addCommentButton)
addCommentButton.addEventListener("click", () => this.handleAddComment())
// Add Comment List //
this.commentList = document.createElement('ul')
card.append(this.commentList)
comments.forEach(comment => this.addCommentLi(comment))
// Create Delete Drink Button
const addDeleteButton = document.createElement('button')
addDeleteButton.setAttribute("class", "btn btn-danger btn-sm")
addDeleteButton.innerText = 'Delete Drink'
card.append(addDeleteButton)
addDeleteButton.addEventListener("click", () => this.handleDeleteDrink(drink, card))
// Connects to Drink //
this.drink = drink
this.cardContent = card;
}
// Helpers //
addCommentLi = comment => {
// Create Li //
const li = document.createElement('li')
this.commentList.append(li)
li.innerText = `${comment.summary}`
// Create Delete Button
const button = document.createElement('button')
button.setAttribute("class", "btn btn-link btn-sm")
button.innerText = 'Delete'
li.append(button)
button.addEventListener("click", () => this.handleDeleteComment(comment, li))
}
// Event Handlers //
// Handle Adding Comment to the DOM //
handleAddComment = () => {
const commentInput = document.getElementById(`commentInput-${this.drink.id}`)
api.addComment(this.drink.id, commentInput.value)
.then(comment => {
commentInput.value = ""
const newComment = new Comment(comment)
Drink.findById(newComment.drinkId).comments.push(newComment)
this.addCommentLi(newComment)
})
}
// Loads last comment created for drink object //
handleLoadComment = () => {
const newComment = this.drink.comments[this.drink.comments.length - 1]
this.addCommentLi(newComment)
}
// Deletes Comment from API and Removes li //
handleDeleteComment = (comment, li) => {
comment.delete()
li.remove()
}
// Deletes Drink from API and Removes drink card //
handleDeleteDrink = (drink, card) => {
drink.delete()
card.remove()
}
}
There are a few ways you can do this:
Pass a reference to a DOM node where you want to append your Drink cards
Get the raw HTML from the card and add it to an element as needed
For (1), try the following changes:
DrinkCard.js
class DrinkCard {
constructor(drink, comments) {
// your existing code
this.cardContent = card; // or card.innerHTML
}
}
Drink.js
class Drink {
// your existing code
render(el) {
// this method will render each card; el is a reference to a DOM node
el.appendChild(this.card.cardContent);
}
}
Finally, passing the DOM reference to the sorted entries:
const node = document.getElementById('rendered-content'); // make sure this exists
sortedArray.forEach(card => card.render(node));
Hopefully that will give you some pointers on how to render the cards for your purpose.
Updates
The error you are getting is because of the following reasons:
First, as you pointed out, an element with id rendered-content does not exist in your DOM
Using .card to append the rendered element results in a cyclical error because you are trying to append an element (.card) to the same element.
You can try the following:
Add <div id="rendered-content"></div> in your HTML somewhere the sorted cards needs to be rendered
If you don't want to have it in the HTML page all the time, you can create it before you pass it's reference. So,
const rc = document.createElement('div');
rc.setAttribute('id', 'rendered-content');
document.body.appendChild(rc);
const node = document.getElementById('rendered-content');
sortedArray.forEach(card => card.render(node));
This should help get rid of the errors hopefully.
Further Explanation
I am going to give you a very brief description of browser rendering and how it's working in this case. I will also leave a link for a detailed article that goes into more depth.
In the rendering flow, the following happens:
Your HTML document is retrieved by the browser and parsed
A DOM tree is then created from the parsed document
Layout is applied to the DOM (CSS)
Paint the content of the DOM to the display
Your code took care of almost everything in the original post. You created the card, added it's content and sorted the cards based on Drink type. The only step missing was adding it all to the DOM.
If you create elements dynamically like you did in the DrinkCard class, you need to attach it to your existing DOM. Otherwise, there is no way for the browser to know your card is in the DOM. Once you modify the DOM, layout and repainting is triggered which then shows your content on the display.
The purpose of div with id='rendered-content' is to provide a container that exists in your DOM or is added before you use it. When you are adding nodes to your DOM, you need a reference element where you want to add your new node. This reference could easily be document.body. In that case, the render method will add the card at the bottom of your body in your DOM. Providing a separate container in this case gives you more control on how you can display this container.
Here's an in depth discussion of rendering and how it works in the browser here. Hope the explanation answers your question.

Assigning a field name from a method to Firestore document

Let's say I have a collection called 'users'. When someone clicks the upvote button, I want it to update the user document with an entry that's the slug from the page they are viewing. However, I'm not sure how to accomplish it using the code I have.
voteUp() {
const voteup_ref = db.collection("builds").doc(this.$route.params.slug)
const voteuser_ref = db.collection("users").doc(this.alias)
return db.runTransaction(t => {
return t.get(voteup_ref).then(doc => {
const newCount = doc.data().voteup + 1
const NewSlug = voteuser_ref
t.update(voteup_ref, {
voteup: newCount
})
t.update(voteuser_ref, {
NewSlug: 'voteup'
})
});
})
},
So, if someone is viewing a page with the slug of 'my-awesome-comment', I want 'my-awesome-comment' to be entered in the user's document with a value of "voteup".
What's actually happening is that it will write an entry to the user document as I expect, but it's entering the field name as 'NewSlug' with a value of "voteup" instead of whatever the slug actually is.
What am I doing wrong?
The way you've written the JSON object NewSlug itself is the property name, instead of using its value. To do the latter, you need to use square bracket notation ([]):
let update = {};
update[NewSlug] = 'voteup';
t.update(voteuser_ref, update)

How to collect and return aggregated data as an Array from a table in Protractor?

I am trying to aggregate a list of dates from a data table, written in Angular, in a Protractor test. I'm doing the aggregation from a PageObject class that is called in the Protractor test. I know that my code is successfully grabbing the text I want, but when I try to console.log the returned array, I get an empty array. I'm still new to Javascript/Typescript, Angular, and Protractor and this may be a result of my newness to the asynchronous nature of this development environment.
Code is as follows,
The PageObject SpecMapper class with method:
import { browser, element, by } from 'protractor';
export class SpecMapperPage {
getImportDateSubmittedColumnValues() {
let stringDatesArray: Array<string> = [];
// currently this css selector gets rows in both import and export tables
// TODO: get better identifiers on the import and export tables and columns
element.all(by.css('md-card-content tbody tr.ng-tns-c3-0')).each(function(row, index){
// check outerHTML for presence of "unclickable", the rows in the export table
row.getAttribute('outerHTML').then(function(outerHTML:string) {
// specifically look for rows without unclickable
if(outerHTML.indexOf("unclickable") < 0){
// grab the columns and get the third column, where the date submitted field is
// TODO: get better identifiers on the import and export columns
row.all(by.css("td.ng-tns-c3-0")).get(2).getText().then(function(text:string) {
stringDatesArray.push(text);
});
}
});
});
return stringDatesArray;
}
}
I know it's not the prettiest code, but it's temporary place holder while my devs make me better attributes/classes/ids to grab my variables. Key things to note is that I create a string Array to hold the values I consider relevant to be returned when the method is finished.
I used WebStorm and put a breakpoint at the stringDatesArray.push(text) and return stringDatesArray lines. The first line shows that the text variable has a string variable that I'm looking for and is successfully getting pushed. I see the success in debug mode as I can see the stringDatesArray and see the values in it. The second line though, the array return, shows that the local variable stringDatesArray is empty. This is echoed in the following code when I try to console.log the array:
The Protractor run Spec class with my test in it:
import { SpecMapperPage } from "./app.po";
import {browser, ExpectedConditions} from "protractor";
describe('spec mapper app', () => {
let page: SpecMapperPage;
let PROJECT_ID: string = '57';
let PROJECT_NAME: string = 'DO NOT DELETE - AUTOMATED TESTING PROJECT';
beforeEach(() => {
page = new SpecMapperPage();
});
describe('import/export page', () => {
it('verify sort order is desc', () => {
browser.waitForAngularEnabled(false);
// Step 1: Launch Map Data from Dashboard
page.navigateTo(PROJECT_ID);
browser.driver.sleep(5000).then(() => {
// Verify: Mapping Screen displays
// Verify on the specmapper page by checking the breadcrumbs
expect(page.getProjectNameBreadCrumbText()).toContain(PROJECT_NAME);
expect(page.getProjectMapperBreadCrumbText()).toEqual("MAPPER");
// Verify: Verify Latest Submitted Date is displayed at the top
// Verify: Verify the Submitted Date column is in descending order
console.log(page.getImportDateSubmittedColumnValues());
});
});
});
});
I acknowledge that this code is not actively using the niceties of Protractor, there's a known issue with our app that will not be addressed for a couple of months, so I am accessing the driver directly 99% of the time.
You'll note that I call the method I posted above as the very last line in the browser.driver.sleep().then() clause, page.getImportDateSubmittedColumnValues().
I thought maybe I was running into asynchronous issues with the call being done before the page was loaded, thus I put it in the .then() clause; but learned with debugging that was not the case. This code should work once I have the array returning properly though.
The console.log is printing an empty [] array. That is synonymous with the results I saw when debugging the above method directly in the PageObject SpecMapper class. I wish to do some verification that the strings are returned properly formatted, and then I'm going to do some date order comparisons. I feel like returning an array of data retrieved from a page is not an unusual request, but I can't seem to find a good way to Google what I'm trying to do.
My apologies if I am hitting some very obvious roadblock, I'm still learning the nuances of Typescript/Angular/Protractor. Thank you for your consideration!
My attempted to used collated promises seemed promising, but fell through on execution.
My Updated PageObject SpecMapper Class
import {browser, element, by, protractor} from 'protractor';
export class SpecMapperPage {
getImportDateSubmittedColumnValues() {
let promisesArray = [];
let stringDatesArray: Array<string> = [];
// This CSS selector grabs the import table and any cells with the label .created-date
element.all(by.css('.import-component .created-date')).each(function(cell, index) {
// cell.getText().then(function(text:string) {
// console.log(text);
// });
promisesArray.push(cell.getText());
});
return protractor.promise.all(promisesArray).then(function(results) {
for(let result of results) {
stringDatesArray.push(result);
}
return stringDatesArray;
});
}
}
My Updated Spec test Using The Updated SpecMapper PO Class
import { SpecMapperPage } from "./specMapper.po";
import {browser, ExpectedConditions} from "protractor";
describe('spec mapper app', () => {
let page: SpecMapperPage;
let PROJECT_ID: string = '57';
let PROJECT_NAME: string = 'DO NOT DELETE - AUTOMATED TESTING PROJECT';
beforeEach(() => {
page = new SpecMapperPage();
});
describe('import/export page', () => {
it('TC2963: ImportComponentGrid_ShouldDefaultSortBySubmittedDateInDescendingOrder_WhenPageIsLoaded', () => {
browser.waitForAngularEnabled(false);
// Step 1: Launch Map Data from Dashboard
page.navigateTo(PROJECT_ID);
browser.driver.sleep(5000).then(() => {
// Verify: Mapping Screen displays
// Verify on the specmapper page by checking the breadcrumbs
expect(page.getProjectNameBreadCrumbText()).toContain(PROJECT_NAME);
expect(page.getProjectMapperBreadCrumbText()).toEqual("MAPPER");
// Verify: Verify Latest Submitted Date is displayed at the top
// Verify: Verify the Submitted Date column is in descending order
page.getImportDateSubmittedColumnValues().then(function(results) {
for(let value of results) {
console.log("a value is: " + value);
}
});
});
});
});
});
When I breakpoint in the PO class at the return stringDatesArray; line, I have the following variables in my differing scopes. Note that the promisesArray has 3 objects, but the results array going into the protractor.promise.all( block has 0 objects. I'm not sure what my disconnect is. :/
I think I'm running into a scopes problem that I am having issues understanding. You'll note the commented out promise resolution on the getText(), and this was my POC proving that I am getting the string values I'm expecting, so I'm not sure why it's not working in the Promise Array structure presented as a solution below.
Only other related question that I could find has to do with grabbing a particular row of a table, not specifically aggregating the data to be returned for test verification in Protractor. You can find it here if you're interested.
As you've alluded to your issue is caused by the console.log returning the value of the variable before its actually been populated.
I've taken a snippet from this answer which should allow you to solve it: Is there a way to resolve multiple promises with Protractor?
var x = element(by.id('x')).sendKeys('xxx');
var y = element(by.id('y')).sendKeys('yyy');
var z = element(by.id('z')).sendKeys('zzz');
myFun(x,y,z);
//isEnabled() is contained in the expect() function, so it'll wait for
// myFun() promise to be fulfilled
expect(element(by.id('myButton')).isEnabled()).toBe(true);
// in a common function library
function myFun(Xel,Yel,Zel) {
return protractor.promise.all([Xel,Yel,Zel]).then(function(results){
var xText = results[0];
var yText = results[1];
var zText = results[2];
});
}
So in your code it would be something like
getImportDateSubmittedColumnValues() {
let promisesArray = [];
let stringDatesArray: Array<string> = [];
// currently this css selector gets rows in both import and export tables
// TODO: get better identifiers on the import and export tables and columns
element.all(by.css('md-card-content tbody tr.ng-tns-c3-0')).each(function(row, index){
// check outerHTML for presence of "unclickable", the rows in the export table
row.getAttribute('outerHTML').then(function(outerHTML:string) {
// specifically look for rows without unclickable
if(outerHTML.indexOf("unclickable") < 0){
// grab the columns and get the third column, where the date submitted field is
// TODO: get better identifiers on the import and export columns
promisesArray.push(row.all(by.css("td.ng-tns-c3-0")).get(2).getText());
}
});
});
return protractor.promise.all(promisesArray).then(function(results){
// In here you'll have access to the results
});
}
Theres quite a few different ways you could do it. You could process the data in that method at the end or I think you could return the array within that "then", and access it like so:
page.getImportDateSubmittedColumnValues().then((res) =>{
//And then here you will have access to the array
})
I don't do the Typescript but if you're just looking to get an array of locator texts back from your method, something resembling this should work...
getImportDateSubmittedColumnValues() {
let stringDatesArray: Array<string> = [];
$$('.import-component .created-date').each((cell, index) => {
cell.getText().then(text => {
stringDatesArray.push(text);
});
}).then(() => {
return stringDatesArray;
});
}
The answer ended up related to the answer posted on How do I return the response from an asynchronous call?
The final PageObject class function:
import {browser, element, by, protractor} from 'protractor';
export class SpecMapperPage {
getImportDateSubmittedColumnValues() {
let stringDatesArray: Array<string> = [];
let promisesArray = [];
// return a promise promising that stringDatesArray will have an array of dates
return new Promise((resolve, reject) => {
// This CSS selector grabs the import table and any cells with the label .created-date
element.all(by.css('.import-component .created-date')).map((cell) => {
// Gather all the getText's we want the text from
promisesArray.push(cell.getText());
}).then(() => {
protractor.promise.all(promisesArray).then((results) => {
// Resolve the getText's values and shove into array we want to return
for(let result of results) {
stringDatesArray.push(result);
}
}).then(() => {
// Set the filled array as the resolution to the returned promise
resolve(stringDatesArray);
});
});
});
}
}
The final test class:
import { SpecMapperPage } from "./specMapper.po";
import {browser, ExpectedConditions} from "protractor";
describe('spec mapper app', () => {
let page: SpecMapperPage;
let PROJECT_ID: string = '57';
let PROJECT_NAME: string = 'DO NOT DELETE - AUTOMATED TESTING PROJECT';
beforeEach(() => {
page = new SpecMapperPage();
});
describe('import/export page', () => {
it('TC2963: ImportComponentGrid_ShouldDefaultSortBySubmittedDateInDescendingOrder_WhenPageIsLoaded', () => {
browser.waitForAngularEnabled(false);
// Step 1: Launch Map Data from Dashboard
page.navigateTo(PROJECT_ID);
browser.driver.sleep(5000).then(() => {
// Verify: Mapping Screen displays
// Verify on the specmapper page by checking the breadcrumbs
expect(page.getProjectNameBreadCrumbText()).toContain(PROJECT_NAME);
expect(page.getProjectMapperBreadCrumbText()).toEqual("MAPPER");
// Verify: Verify Latest Submitted Date is displayed at the top
// Verify: Verify the Submitted Date column is in descending order
page.getImportDateSubmittedColumnValues().then((results) => {
console.log(results);
});
});
});
});
});
The biggest thing was waiting for the different calls to get done running and then waiting for the stringDataArray to be filled. That required the promise(resolve,reject) structure I found in the SO post noted above. I ended up using the lambda (()=>{}) function calls instead of declared (function(){}) for a cleaner look, the method works the same either way. None of the other proposed solutions successfully propagated the array of strings back to my test. I'm working in Typescript, with Protractor.

How to remove mentions from an editorstate in draft js?

Been at this one for a LONG time and can't quite figure it out.
I have two components, controlled by a parent component. There is a property called "selected". So, when a user clicks on a list, it will update the parent component's selected property which is passed to the TagInput, which uses a MentionPlugin from draft-js.
In order to handle this, I implement a componentWillReceiveProps that looks as follows.
componentWillReceiveProps(nextProps) {
const { initialTags: newTags } = nextProps;
const previousTags = this.getTags(this.state.editorState);
if (previousTags.length !== newTags.length) {
const added = newTags.filter(tag => !previousTags.includes(tag));
const removed = previousTags.filter(tag => !newTags.includes(tag));
this.addMentions(added);
this.removeMentions(removed);
}
}
While it's easy to add entities in addMentions by creating a new entity and inserting it, for the life of me, I cannot figure out how to get a Mention by text and then delete it from the editor.
removeMentions(tags) {
const { editorState } = this.state;
for (const tag of tags) {
// find tag in editor
// select it and remove it
}
}
How would this be done?

Categories

Resources