Angular 2 component global variable is undefined? - debugging

Im trying to create an if statement within a template to display a certain block of text when an array length is < 1.
This is how I try to do it:
<div *ngIf="socialNetworks.length > 1">
<div class="alert-box warning radius">You can still connect some accounts</div>
<button class="btn btn-primary" *ngFor="let network of socialNetworks" (click)="loginSocialNetwork(network)">
{{network.title}}
</button>
</div>
But I always get an error saying that it cannot read property length of undefined.
I define the variable socialNetworks[] here in an Angular 2 component:
export class MyAccountComponent implements OnInit {
socialNetworks: socialNetwork[];
campaigns: Campaign[];
showGreeting: boolean = true;
constructor(
private _dataservice: DataService) {
}
Then, in a seperate method, I set the value from a response from a pyramid view here:
getSocialNetworks() {
var url: string;
url = "/account_api"
this._dataservice.getDataByUrl(url)
.subscribe(
res => this.socialNetworks = res,
err => this.logError(err)
)
}
Even if I add a console.log statement at the end here to see the value of this.socialNetworks, it says it is undefined. But in a debugger I can see that the value of this.socialNetworks is not undefined.
So my question is, am I just referencing the global variable incorrectly, or am I missing/misunderstanding something all together?

Looks to me like socialNetworks isn't set as an empty array on construction, so it will be undefined on init. Try to change the top socialNetwork to:
socialNetworks: socialNetwork[] = [];
The issue we were speaking about below in the comments is most likely to do with this within the subscribe method. It is assigning it to the incorrect scope.
try below:
getSocialNetworks() {
var _that = this;
var url: string;
url = "/account_api"
this._dataservice.getDataByUrl(url)
.subscribe(
res => _that.socialNetworks = res,
err => _that.logError(err)
)
}

If the api call uses some external library it might not be hooked into angular 2's digest cycle (Facebook SDK isn't for example). You can debug using Augury, or for simplicity set a global window property in your constructor to access your component:
constructor(private _dataservice: DataService) {
window['MAC'] = this;
}
Then you can check in the dev tools of your browser to see if the socialNetworks array is actually being set:
MAC.socialNetworks
You can always in your HTML use some quick code to show it as JSON to debug what is going on:
{{socialNetworks | json}}
If the page doesn't show it having a value but you see it in the console, then your _dataService isn't triggering change detection and you have to do it manually. You can use ApplicationRef by injecting it in your constructor and calling tick() on it after you set your data.
constructor(
private _dataservice: DataService,
private _appref: ApplicationRef ) {
}
getSocialNetworks() {
var _that = this;
var url: string;
url = "/account_api"
this._dataservice.getDataByUrl(url)
.subscribe(
res => {
_that.socialNetworks = res;
this._appref.tick(); // force change detection
}, err => _that.logError(err)
)
}
As for the calling length on undefined, since you're asynchronously pulling data your socialNetworks property is undefined to start. You can initialize it to an empty array like in the answer by #JacobS or modify your check to account for it:
<div *ngIf="socialNetworks && socialNetworks.length > 1">

Related

Storybook problem while migrating argument of type object from addon-knobs to addon-controls

I'm having some trouble migrating one thing from the old addon-knobs to the new controls. Let me explain, maybe it's not such difficult task but I'm blocked at the moment.
I'm using StencilJS to generate Web Components and I have a custom select component that accepts a options prop, this is an array of objects (the options of the select)
So, the story for this component in the previous version of Storybook looks something like this:
export const SelectWithArray = () => {
const selectElement = document.createElement('my-select');
selectElement.name = name;
selectElement.options = object('Options', options);
selectElement.disabled = boolean('Disabled', false);
selectElement.label = text('Label', 'Label');
return selectElement;
};
This works fine, the select component receives the options property correctly as an array of objects.
Now, migrating this to the new Storybook version without addon-knobs, the story is looking like this:
const TemplateWithArray: Story<ISelect> = (args) => {
return `
<my-select
label="${args.label}"
disabled="${args.disabled}"
options="${args.options}"
>
</my-select>
`;
};
export const SelectWithArray: Story<ISelect> = TemplateWithArray.bind({});
SelectWithArray.argTypes = {
options: {
name: 'Options',
control: { type: 'object' },
}
}
SelectWithArray.args = {
options: [
{ text: 'Option 1', value: 1 },
]
}
And with this new method, the component is not able to receive the property as expected.
I believe the problem is that now, the arguments is being set directly on the HTML (which would only be accepting strings) and before it was being set on the JS part, so you could set attributes other than strings.
Is there a way to achieve this? without having to send the arguments as a string.
Thanks a lot!!
One way I've discovered so far is to bind the object after the canvas has loaded via the .play function;
codeFullArgs.play = async () => {
const component = document.getElementsByTagName('your-components-tag')[0];
component.jobData = FullArgs.args.jobData;
}

Cypress get attribute value and assign it to variable in function

I am trying to get attribute value and return it from a function.
Here is the code that is working and can be used in a normal test class (into the integration folder).
describe('Example shows how to get attribute value.', () => {
// 'it' is used to create test case. You can add a name of the test case. You can have multiple test cases in one JS class.
it('Get attribute value.', () => {
// Cypress is not able to work with new tabs. It is not possible to switch between tabs. Cypress can manipulate the DOM tree, so we can change the element attributes and open the hyperlink in the same browser tab.
// 'visit()' method is used for navigating to URL address.
cy.visit('https://demoqa.com/links')
cy.xpath('//*[#id="simpleLink"]').then(function (element) {
// 'prop()' method is used to get the attribute value.
const url = element.prop('href')
cy.visit(url)
})
// Assert URL.
cy.url().should('include', 'demoqa.com').should('eq', 'https://demoqa.com/');
})
If I use the code that way - everything is working as expected.
But if I want to re-use the code and create a function like this:
// Give a value of the variable to use it for next function.
functionName = 'addAttribute';
// Declare a Cypress child custom command.
Cypress.Commands.add(functionName, { prevSubject: 'element' }, (subject: any, attributeName: string, attributeValue: string) => {
// Create a try-catch statement. If the function fails - we will recieve the error message.
try {
// Create the function steps after this comment.
cy
.wrap(subject)
.invoke('attr', attributeName, attributeValue)
.should('have.attr', attributeName, attributeValue)
} catch (error) {
// Create the error log and show it to the UI. Show the function name, the class where the function is located and catched error.
let errorMessage = `----------ERROR! It seems that we have an error. Please review the "${functionName}" function from "${__filename.split(__dirname + "/").pop()}" . The error is: ${error}`;
cy.log(errorMessage);
console.log(errorMessage);
}
})
The result is 'object' and I am not sure how to process it.
Here is the rest of the code:
describe("'getAttribute' custom child command example.", () => {
it("example shows how to use 'getAttribute' custom child command.", () => {
cy.visit('https://demoqa.com/buttons');
let attributeValue = cy.element('xpath','(//*[contains(text(),"Click Me")])[3]').getAttribute('class');
cy.log(`The attribute values is: ${attributeValue}`)
});
});
You have to do as below to have a return value:
let attributeValue = '';
cy.element('xpath', '(//*[contains(text(),"Click Me")])[3]')
.getAttribute('class')
.then((attr) => {
attributeValue = attr;
});
cy.log('The attribute values is:' + attributeValue)

Accessing reactive properties with vee-validate 4

I am getting my head around Vee-Validate next (v4) and how I might incorporate it in a Vue 3 project without loosing Vue's reactivity (i.e. not relying on the values simply being passed to the Form submit event).
By way of example, if I were making a hypothetical component which has autocomplete functionality, and sent a get request to the server once 3 letters had been typed, but for the input itself to be valid it required 8 letters, how would I get the value associated with the input?
using plain Vue, with pseudo-code something like:
defineComponent({
setup () {
const myVal = ref('')
const options = ref([])
watchEffect(() => if (myVal.value.length > 3) {
axios.get(...).then(serverVals => options.value = serverVals))
})
return { myVal, options }
how would I achieve this with vee-validate 4.x?
defineComponent({
setup () {
const schema = yup.object({ myVal: yup.string().required().min(8) })
// ???? what now to watch myVal
please note this is not about autocomplete - a range slider where I wanted a server call when the value was greater than 10 but a validation message if greater than 90 would also suffice as an example.
You could employ useField here to get a reactive value that's automatically watched.
const { value: myVal, errorMessage } = useField('myVal', undefined, {
initialValue: ''
});
const options = ref([])
watchEffect(() => if (myVal.value.length > 3) {
axios.get(...).then(serverVals => options.value = serverVals))
})
return { myVal, options }
Documentation has an example of using useField:
https://vee-validate.logaretm.com/v4/guide/composition-api#usefield()
Note that you don't have to use useForm, if you are using <Form> component and passing schema to it then that should work just fine.

Passing text from HTML as variable using protractor

I have a table with unknown values (which I need to log in later) and I would like to save one of it with class currentWeek to a variable. HTML is as following:
<tr _ngcontent-c21="" class="currentWeek">
<th _ngcontent-c21="" class="text-center">Value1Foo</th>
(...)
</tr>
In proctracor I created in Helper.ts:
static getfooOfTheWeek() {
let child = element(by.css('.currentWeek')).$('.text-center');
describe('Get foo', function () {
it('get foo', function () {
browser.driver.get('tablepage');
browser.sleep(3000);
return ((child).getText()).toString();
})
})
}
and in file maintest.ts:
describe('Get FOO', function () {
var FOO=Helper.getfooOfTheWeek();
browser.sleep(2000);
//use the value set in Helper.getfooOfTheWeek();
NegativeTest.SomeTest(FOO);
});
but it fails ususally with - Failed: each key must be a number of string; got undefined - therefore I think the FOO is save as an Object, not as string.
I thought also using JSON (to use JSON.parse()), but developers can't gives the values to the table from JSON
Any path what I can try?
Got it - found this issue about saving as string instead of object: Protractor: element.getText() returns an object and not String where I need to resolve the promise
and instead of passing it as variable with var FOO=Helper.getfooOfTheWeek(); I saved it as browser.params.Foo
static getfooOfTheWeek() {
let child = element(by.css('.currentWeek')).$('.text-center');
browser.driver.get('tablepage');
browser.sleep(3000);
child.getText().then(function(text) {
browser.params.Foo=text;
});
}
and I can run NegativeTest.SomeTest(); where the variable is passed in SomeTest() as browser.params.Foo (instead of writting there FOO)

Angular2 dart ngFor not updating view when using WebSockets

I've been having a problem with an angular2 component. The view doesn't update when I'm using websockets. I've tried with http requests and it works fine, but I need to keep the data in the view updated.
I can connect to the server just fine using websockets and I can recieve the data, but my view doesn't update.
#Component(
selector: "pv-table",
templateUrl: "./table.component.html"
)
class TableComponent
{
WebSocket _connection;
// NgZone _zone;
// ApplicationRef _application_ref;
List data;
TableComponent(/* this._zone, this._application_ref */)
{
_connection = new WebSocket( "ws://${HOST}:${PORT}" );
_connection.onOpen.first.then( ( _ ) {
_connection.onMessage.listen( ( MessageEvent e ) => OnMessage( e.data ) );
// A simple class that gives my application a standard
// way to communicate.
Packet request = new Packet()
..message = "table.get"
..data = {};
_connection.send( JSON.encode( request ) );
_connection.onClose.first.then( ( _ ) { });
});
}
void OnMessage( String data )
{
Map res = JSON.decode( data );
String cmd = res[ "message" ];
Log.Info( cmd );
switch( cmd )
{
case "table.send":
// Setting the data here. This works
data = res[ "data" ];
// This prints the correct data in the developer console.
Log.Info( data );
// Neither of these help.
// _zone.run( () => data = res[ "data" ] );
// or
// _application_ref.tick();
break;
default:
Log.Warn( "Unknown command: $cmd" );
break;
}
}
}
I've googled around and seen some problems like this where forcing change decection with NgZone.run(...) or ApplicationRef.tick(), but that didn't help. Either they don't work for this situation or I don't know how to use them.
My template looks like this:
<p *ngFor="let person of data">
<span scope="row">{{ person[ "_id" ] }}</span>
<span>{{ person[ "name" ] }}</span>
<span>{{ person[ "job" ] }}</span>
<span>{{ person[ "date_due" ] }}</span>
</p>
As a comment mentions. I can see the data being printed in the developer console. It is in the correct format, a list of maps with the correct fields, but the page remains blank.
Turns out I was just being really, really stupid.
Lets just take a look at a few things shall we?
The signature for the method that handles incoming data:
void OnMessage( String data )
The member that I am trying to use in the template:
List data;
The assignment inside of the OnMessage method:
data = res[ "data" ];
Notice anything strange?
Maybe that the class member and the method parameter have the same name?
Maybe that means the parameter is shadowing the member?
Maybe that means an assignment to that name is actually to the parameter and not the member?
The worst part is I've been sat here for almost two hours trying to figure out what the problem was.
Changing two lines and everything works
void OnMessage( String data ) => void OnMessage( String response_data )
Map res = JSON.decode( data ); => Map res = JSON.decode( response_data );

Resources