How do I test the following v-if on my parent component using Jest?
Parent:
<div class="systemIsUp" v-if="systemStatus == true">
foo
</div>
<div class="systemIsDown" v-else>
bar
</div>
<script>
export default {
name: 'File Name',
data () {
return {
systemStatus: null,
}
},
</script>
Here is my current setup for testing if those divs render when I change the value of the systemStatus variable
Unit Test:
import { shallowMount } from '#vue/test-utils'
import FileName from 'path'
describe('FileName', () => {
//Declare wrapper for this scope
const wrapper = shallowMount(FileName)
it('Should display message saying that the system is down if "systemStatus" data variable is false', () => {
expect(wrapper.html().includes('.systemIsDown')).toBe(false)
wrapper.setData({ systemStatus: false})
expect(wrapper.html().includes('.systemIsDown')).toBe(true)
});
});
I have tried using contains and toContain instead of includes but still cannot get it to work, Jest returns the following:
expect(received).toBe(expected) // Object.is equality
Expected: true
Received: false
expect(wrapper.html().includes('.systemIsDown')).toBe(false)
wrapper.setData({ systemStatus: false })
expect(wrapper.html().includes('.systemIsDown')).toBe(true)
^
Clearly it cannot see the systemIsDown div at all and does not think it exists hence why the first expect passes but how can I get it to see the div when the systemStatus variable is updated?
Thanks
Changed around the assertion to look for specific CSS selector as follows:
wrapper.setData({ systemStatus: false})
expect(wrapper.find(".systemIsDown")).toBeTruthy()
This should work, if you want to check given DOM element has been created (or not):
expect(wrapper.find(".systemIsDown").exists()).toBe(true) // or false
more here
I have a custom element, x-custom, that has a property, object, that is an Object.
I have another property, name, that I want to use to index object, to put data from other custom elements under a specific property of object.
Something like the following is the end goal:
<other-custom-element data="{{object.{{name}}}}"></other-custom-element>
...
name: {
type: String,
},
object: {
type: Object,
value: {}
}
So, in the example, if the value of name is "prop1", then the data from other-custom-element would be placed into object.prop1.
EDIT:
So, I've updated my code and have gotten a bit closer, but still not quite all the way. The following example code is much closer to my actual code, so maybe it'll help in answering the question:
<template is="dom-repeat" items="[[locations]]" as="location">
<get-data-at-location
location="[[location]]"
data="[[_resolveRef(location)]]">
</get-data-at-location>
<h5>Data at location: [[location]]</h5>
<template is="dom-repeat" items="[[_resolveRef(location)]]" as="data">
<p>Data: {{data.value}}</p>
<p>Time: {{data.time}}</p>
</template>
</template>
...
alldata: {
type: Object
value: {}
},
_resolveRef: function(location) {
return this.alldata[location];
}
I'm trying to basically pass a reference to a specific element of alldata with _resolveRef(), but I think there may be a scoping issue. I get the following error (say, if location is equal to "some_location"):
Uncaught TypeError: Cannot read property 'some_location' of undefined
So, for some reason it doesn't seem to know about "alldata", even though it is a property of the custom element. Hopefully that's enough for a Polymer whiz to help :)
EDIT 2:
Some sample data to make the final goal more clear.
Say that locations is an array consisting of the following strings:
locations: {
type: Array,
value: ['location1', 'location2']
}
And say that the <get-data-at-location> element returns the following data for each individual location:
<get-data-at-location
location=[[location]] //'location1'
data={{}}> //[{value: 4, time: 0}, {value: 5, time: 1}]
</get-data-at-location>
<get-data-at-location
location=[[location]] //'location2'
data={{}}> //[{value: 10, time: 1}, {value: 11, time: 2}]
</get-data-at-location>
After all is said and done, I would like alldata to have the following information:
alldata: {
location1: [{value: 4, time: 0}, {value: 5, time: 1}],
location2: [{value: 10, time: 1}, {value: 11, time: 2}]
}
So, I would like the object alldata shared across all instances of the dom-repeat so that they can all place data into their respective property (which is just the string value of location) of the overall object.
EDIT 3:
Tried something new. Rather than _resolveRef(location) as a way to return a pointer to the location where <get-data-from-location> should store the data that it gets, I just used the following syntax: {{alldata[location]}} and things worked better, but still not perfect.
<template is="dom-repeat" items="[[locations]]" as="location">
<get-data-at-location
location="[[location]]"
data="{{alldata[location]}}">
</get-data-at-location>
<h5>Data at location: [[location]]</h5>
<template is="dom-repeat" items="{{alldata[location]}}" as="data">
<p>Data: {{data.value}}</p>
<p>Time: {{data.time}}</p>
</template>
</template>
...
alldata: {
type: Object
value: {}
}
So, this appears to work! Until I realized that the alldata that the data is being bound to is not the alldata of the custom element - it appears to be bound to the scope of the first dom-repeat, which the second dom-repeat has access to.
So basically, if I was to use this custom element within another page and bind to alldata, it doesn't work:
<x-custom alldata={{allData}}></x-custom>
<x-pretty-json object={{allData}}></x-pretty-json>
<x-pretty-json> just pretty-prints the provided object, and in this case it just prints empty brackets {}. So, closer (kind of), but still not quite there.
In this example (Plunk) there is bind between property and array item.
firstName should change from 'John' to 'Test' on click, but it's not happening.
How to make the property to change on item update?
<script src="http://www.polymer-project.org/1.0/samples/components/webcomponentsjs/webcomponents-lite.min.js"></script>
<link rel="import" href="http://www.polymer-project.org/1.0/samples/components/polymer/polymer.html">
<!--
<link rel="import" href="https://cdn.rawgit.com/download/polymer-cdn/1.1.4/lib/paper-input/paper-input.html" />
-->
<dom-module id="first-el">
<template>
<br> firstName should change on click:<br><br>
firstName: <span>{{employees.employees.0.firstName}}</span>
<br>
<br>
<button on-tap="tap_change_firstName_1">change firstName to: Test</button>
</template>
<script>
(function() {
'use strict';
Polymer({
is: "first-el",
properties: {
employees: {
type: Object,
notify: true,
},
},
//domReady:
attached: function() {
this.async(function() {
console.log('first: domReady:');
this.employees = {
"employees": [{
"firstName": "John",
"lastName": "Doe"
}]
};
});
},
tap_change_firstName_1: function() {
console.log('First: tap_change_firstName_1');
//update array item property
this.set('employees.employees.0.firstName', 'Test');
console.log('New value:');
console.log(this.employees.employees[0].firstName);
//here the value is cnahged but that does not reflect in the DOM
},
});
})();
</script>
</dom-module>
<!-- use the element -->
<first-el></first-el>
Update:
array-selector (simple example) element can be used for this task too.
The set() convenience function just wraps the property setter and the notifyPath call in one. When your data is an array like that, I believe notifyPath is expecting the upper-level array itself and not just a single slice of it.
One way to resolve it (there are probably a few) would be to make that notifyPath call yourself after setting the property directly.
this.employees.employees[0].firstName = 'Test';
this.notifyPath('employees.employees', this.employees.employees);
See new Plunker.
Upper solution works only for one change, it does not work for multiple 'name' updates, example: Plunk
Correct solution, from the docs: in this Plunk
Explicit bindings to array items by index isn’t supported
<div>{{arrayItem(myArray.*, 0, 'name')}}</div>
...
// first argument is the change record for the array change,
// change.base is the array specified in the binding
arrayItem: function(change, index, path) {
// this.get(path, root) returns a value for a path
// relative to a root object.
return this.get(path, change.base[index]);
},
...
// change a subproperty
this.set('myArray.1.name', rnd_firstName);
I have setup two way binding with a normal textarea in Polymer using:
<textarea id="textbox" value="{{editText::input}}" autofocus></textarea>
I've also tried a two way binding iron-autogrow-textarea using the bindValue attribute:
<iron-autogrow-textarea bindValue="{{editText}}" class="fit" autofocus></iron-autogrow-textarea>
The property editText is assigned as follows:
Polymer({
is: "page-editor",
properties: {
editText: {
type: String,
value: ""
}
},
But when change the editText in code below it won't update the respective textarea values...
this.editText = "new message";
Interestingly a console.log(this.editText) says its 'undefined'
The correct attribute to use is bind-value="{{editText}}". CamelCase properties are translated to attributes with dashes (source).
I'm still ramping up on Polymer, but I think you need to set notify to true.
Polymer({
is: "page-editor",
properties: {
editText: {
type: String,
value: "",
+ notify: true
}
},
...
If that doesn't work, post a full sample and I'll be happy to debug with you.
You can add an event listener with Polymer's on-* syntax, where * is the event to listen for.
<iron-autogrow-textarea bindValue="{{editText}}"
class="fit"
on-click="f'
autofocus></iron-autogrow-textarea>
Define the event listener:
Polymer({
is: "page-editor",
properties: {
editText: {
type: String,
value: "",
notify: true
}
},
/* the function signature below may be wrong...
* don't know how many arguments it takes... */
f: function(e, detail, sender) {
this.editText = 'yay';
}
...
The Polymer 1.0 documentation states:
The path syntax doesn’t support array-style accessors (such as
users[0].name). However, you can include indexes directly in the path
(users.0.name).
How would one get around this in setting the path dynamically, and obtain the same behavior as the following example using Polymer 0.5? This is specifically in the context of generating forms for a model defined by an Object.
<template repeat="{{row in fieldset.rows}}">
<div layout horizontal flex>
<template repeat="{{field in row}}" flex>
<paper-field field="{{model.fields[field]}}" value="{{obj[field]}}">
</paper-field>
</template>
</div>
</template>
edit:
Per https://github.com/Polymer/polymer/issues/1504:
No near-term plans to support this. Polymer 0.5 had a complex expression parser used for bindings that we have eliminated for simplicity and performance. There are alternate patterns you can use today to achieve similar results that just require you to be more explicit.
What the alternate pattern would be to achieve two way data binding remains unclear.
Yes, it is true that Polymer 1.0 no longer supports myObject[key] in binding expressions. However, in your particular use-case, there are ways to sidestep this problem.
One-way data-binding
It is fairly simple to overcome this limitation when it comes to one-way data-binding. Simply use a computed property that accepts both the object and the key in question:
<my-element value="[[getValue(obj, key)]]"></my-element>
getValue: function(obj, key) {
return obj[key];
}
Two-way data-binding
In the case of two-way data-binding, it is still possible to create a functional alternative to the binding expression {{obj[key]}} in Polymer 1.0. However, it will require taking into consideration the particular use-case in which you are hoping to implement the binding.
Taking into account the example in your question, it seems that you are doing work with some sort of table or fieldset. For the purposes of the example here, I will use a slightly different, yet very similar structure.
Let's assume that we have a fieldset object, and that this object is structured as such:
{
"fields": [
{ "name": "Name", "prop": "name" },
{ "name": "E-mail", "prop": "email" },
{ "name": "Phone #", "prop": "phone" }
],
"rows": [
{
"name": "John Doe",
"email": "jdoe#example.com",
"phone": "(555) 555-1032"
},
{
"name": "Allison Dougherty",
"email": "polymer.rox.1337#example.com",
"phone": "(555) 555-2983"
},
{
"name": "Mike \"the\" Pike",
"email": "verypunny#example.com",
"phone": "(555) 555-7148"
}
]
}
If we want to create some sort of table-like output which represents this object, we can use two nested repeating templates: the first to iterate through the different rows, and the second to iterate through the different fields. This would be simple using the one-way data-binding alternative above.
However, achieving two-way data-binding is very different in this case. How is it do-able?
In order to understand how to come up with a solution, it is important to break down this structure in a way that will help us figure out what observation flow we should implement.
It becomes simple when you visualize the fieldset object like this:
fieldset
--> rows (0, 1, ...)
--> row
--> fields (name, email, phone)
--> value
And becomes even simpler when you factor in the workflow of your element/application.
In this case, we will be building a simple grid editor:
+---+------+--------+-------+
| | Name | E-mail | Phone |
+---+------+--------+-------+
| 0 | xxxx | xxxxxx | xxxxx |
+---+------+--------+-------+
| 1 | xxxx | xxxxxx | xxxxx |
+---+------+--------+-------+
| 2 | xxxx | xxxxxx | xxxxx |
+---+------+--------+-------+
There are two basic ways that data will flow in this app, triggered by different interactions.
Pre-populating the field value: This is easy; we can easily accomplish this with the nested templates mentioned earlier.
User changes a field's value: If we look at the application design, we can infer that, in order to handle this interaction, whatever element we use as an input control, it must have some sort of way of knowing:
The row it will affect
The field it represents
Therefore, first let's create a repeating template which will use a new custom element that we will call basic-field:
<div class="layout horizontal flex">
<div>-</div>
<template is="dom-repeat" items="[[fieldset.fields]]" class="flex">
<div class="flex">[[item.name]]</div>
</template>
</div>
<template is="dom-repeat" items="{{fieldset.rows}}" as="row" index-as="rowIndex">
<div class="layout horizontal flex">
<div>[[rowIndex]]</div>
<template is="dom-repeat" items="[[fieldset.fields]]" class="flex">
<basic-field class="flex" field="[[item]]" row="{{row}}"></basic-field>
</template>
</div>
</template>
(In the snippet above, you'll notice that I've also added a separate repeating template to generate the column headers, and added a column for the row index - this has no influence on our binding mechanism.)
Now that we've done this, let's create the basic-field element itself:
<dom-module>
<template>
<input value="{{value}}">
</template>
</dom-module>
<script>
Polymer({
is: 'basic-field',
properties: {
row: {
type: Object,
notify: true
},
field: {
type: Object
},
value: {
type: String
}
}
});
</script>
We have no need to modify the element's field from within the element itself, so the field property does not have notify: true. However, we will be modifying the contents of the row, so we do have notify: true on the row property.
However, this element is not yet complete: In its current state, it doesn't do any of the work of setting or getting its value. As discussed before, the value is dependent on the row and the field. Let's add a multi-property observer to the element's prototype that will watch for when both of these requirements have been met, and populate the value property from the dataset:
observers: [
'_dataChanged(row.*, field)'
],
_dataChanged: function(rowData, field) {
if (rowData && field) {
var value = rowData.base[field.prop];
if (this.value !== value) {
this.value = value;
}
}
}
There are several parts to this observer that make it work:
row.* - If we had just specified row, the observer would only be triggered if we set row to an entirely different row, thus updating the reference. row.* instead means that we are watching over the contents of row, as well as row itself.
rowData.base[field.prop] - When using obj.* syntax in an observer, this tells Polymer that we are using path observation. Thus, instead of returning just the object itself, this means that rowData will return an object with three properties:
path - The path to the change, in our case this could be row.name, row.phone, etc.
value - The value that was set at the given path
base - The base object, which in this case would be the row itself.
This code, however, only takes care of the first flow - populating the elements with data from the fieldset. To handle the other flow, user input, we need a way to catch when the user has inputted data, and to then update the data in the row.
First, let's modify the binding on the <input> element to {{value::input}}:
<input value="{{value::input}}">
Polymer does not fully automate binding to native elements as it doesn't add its own abstractions to them. With just {{value}}, Polymer doesn't know when to update the binding. {{value::input}} tells Polymer that it should update the binding on the native element's input event, which is fired whenever the value attribute of the <input> element is changed.
Now let's add an observer for the value property:
value: {
type: String,
observer: '_valueChanged'
}
...
_valueChanged: function(value) {
if (this.row && this.field && this.row[this.field] !== value) {
this.set('row.' + this.field.prop, value);
}
}
Notice that we aren't using this.row[this.field.prop] = value;. If we did, Polymer would not be aware of our change, as it does not do path observation automatically (for the reasons described earlier). The change would still be made, but any element outside of basic-field that may be observing the fieldset object would not be notified. Using this.set gives Polymer a tap on the shoulder that we are changing a property inside of row, which allows it to then trigger all the proper observers down the chain.
Altogether, the basic-field element should now look something like this (I've added some basic styling to fit the <input> element to the basic-field element):
<link rel="import" href="components/polymer/polymer.html">
<dom-module id="basic-field">
<style>
input {
width: 100%;
}
</style>
<template>
<input value="{{value::input}}">
</template>
</dom-module>
<script>
Polymer({
is: 'basic-field',
properties: {
row: {
type: Object,
notify: true
},
field: {
type: Object
},
value: {
type: String,
observer: '_valueChanged'
}
},
observers: [
'_dataChanged(row.*, field)'
],
_dataChanged: function(rowData, field) {
if (rowData && field) {
var value = rowData.base[field.prop];
if (this.value !== value) {
this.value = value;
}
}
},
_valueChanged: function(value) {
if (this.row && this.field && this.row[this.field] !== value) {
this.set('row.' + this.field.prop, value);
}
}
});
</script>
Let's also go ahead and take the templates we've made before and throw them into a custom element as well. We'll call it the fieldset-editor:
<link rel="import" href="components/polymer/polymer.html">
<link rel="import" href="components/iron-flex-layout/iron-flex-layout.html">
<link rel="import" href="basic-field.html">
<dom-module id="fieldset-editor">
<style>
div, basic-field {
padding: 4px;
}
</style>
<template>
<div class="layout horizontal flex">
<div>-</div>
<template is="dom-repeat" items="[[fieldset.fields]]" class="flex">
<div class="flex">[[item.name]]</div>
</template>
</div>
<template is="dom-repeat" items="{{fieldset.rows}}" as="row" index-as="rowIndex">
<div class="layout horizontal flex">
<div>[[rowIndex]]</div>
<template is="dom-repeat" items="[[fieldset.fields]]" class="flex">
<basic-field class="flex" field="[[item]]" row="{{row}}"></basic-field>
</template>
</div>
</template>
<pre>[[_previewFieldset(fieldset.*)]]</pre>
</template>
</dom-module>
<script>
Polymer({
is: 'fieldset-editor',
properties: {
fieldset: {
type: Object,
notify: true
}
},
_previewFieldset: function(fieldsetData) {
if (fieldsetData) {
return JSON.stringify(fieldsetData.base, null, 2);
}
return '';
}
});
</script>
You'll notice we're using the same path observation syntax as we did inside of basic-field to observe changes to fieldset. Using the _previewFieldset computed property, we will generate a JSON preview of the fieldset whenever any change is made to it, and display it below the data entry grid.
And, using the editor is now very simple - we can instantiate it just by using:
<fieldset-editor fieldset="{{fieldset}}"></fieldset-editor>
And there you have it! We have accomplished the equivalent of a two-way binding using bracket-notation accessors using Polymer 1.0.
If you'd like to play with this setup in real-time, I have it uploaded on Plunker.
You can make a computed binding. https://www.polymer-project.org/1.0/docs/migration.html#computed-bindings
<paper-field field="{{_computeArrayValue(model.fields, field)}}" value="{{_computeArrayValue(obj, field}}"></paper-field>
<script>
Polymer({
...
_computeArrayValue: function(array, index) {
return array[index];
},
...
});
</script>
As an aside, you also need to update your repeat to dom-repeat https://www.polymer-project.org/1.0/docs/devguide/templates.html#dom-repeat
Edit: Here is my ugly solution to the 2-way binding. The idea is that you have a calculated variable get the initial value and then update this variable upon updates with an observer.
<!-- Create a Polymer module that takes the index and wraps the paper field-->
<paper-field field="{{fieldArrayValue}}" value="{{objArrayValue}}"></paper-field>
<script>
Polymer({
...
properties: {
fields: { //model.fields
type: Array,
notify: true
},
obj: {
type: Array,
notify: true
},
arrayIndex: {
type: Number,
notify: true
},
fieldArrayValue: {
type: String,
computed: '_computeInitialValue(fields, number)'
},
objArrayValue: {
type: String,
computed: '_computeInitialValue(obj, number)'
}
},
_computeInitialValue: function(array, index) {
return array[index];
},
observers: [
'fieldsChanged(fields.*, arrayIndex)',
'objChanged(fields.*, arrayIndex)'
],
fieldsChanged: function (valueData, key) {
this.set('fieldArrayValue', this.fields[this.arrayIndex]);
},
objChanged: function (valueData, key) {
this.set('objArrayValue', this.obj[this.arrayIndex]);
},
...
});
</script>
Edit 2: Updated the code in Edit 1 to reflect the obeserver changes pointed out by Vartan Simonian