Strange behavior in Polymer data-binding to an attribute - javascript

Using Polymer 1.0 I'm trying to bind to an attribute of a custom element, and just display it.
The custom element is in fact an <iron-input> list, that has an add and a delete button. I'd like to reflect any change in that list to the host. It also has a minItemSize attribute meaning it has at least this many elements. So I added a check to the observer, adding extra elements in case it goes under this number.
But when I bind to the attribute that holds the list, things get out of sync, and I can delete all of the inputs from the ui.
I have two <dyn-inputlist> elements. In one of them I don't bind to the data
attribute, in the other I do.
The first one behaves as expected: adds and removes on the button click.
The other doesn't work, because you can remove all input boxes. Even though the data itself is updated, and filled with extra items, for some reason the UI doesn't reflect this. (Checking the data property of the element does show that it has the correct number of items)
I also expect that if I set data={{myData}} on both dyn-inputlist element, they always display the same thing. But pressing add/remove buttons randomly on either component gets them out of sync.
Am I missing something?
Thanks in advance.
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="bower_components/webcomponentsjs/webcomponents.js"></script>
<link rel="import" href="components/dyn-inputlist.html"/>
</head>
<body>
<template is="dom-bind">
<dyn-inputlist min-item-size="4"></dyn-inputlist>
<div>{{mydata}}</div>
<dyn-inputlist min-item-size="4" data="{{mydata}}"></dyn-inputlist>
</template>
</body>
</html>
dyn-inputlist.html:
<link rel="import" href="../../polymer/polymer.html">
<link rel="import" href="../../iron-input/iron-input.html">
<dom-module id="dyn-inputlist">
<template>
<button on-click="removeItem">x</button>
<button on-click="addItem">+</button>
<template is="dom-repeat" items="{{data}}">
<div>
<span>{{index}}</span>
<input is="iron-input" bind-value="{{item.content}}">
</div>
</template>
</template>
<script>
Polymer({
is: 'dyn-inputlist',
properties: {
minItemSize: {
type: Number,
notify: true,
value: 1
},
data: {
type: Array,
reflectToAttribute: true,
notify: true,
value: function () {
return []
}
}
},
observers: ['_dataChanged(data.*)'],
addItem: function (e) {
this.unshift('data', {content: ""});
this.reflectPropertyToAttribute('data')
},
removeItem: function (e) {
this.shift('data');
this.reflectPropertyToAttribute('data')
},
_dataChanged: function (e) {
if (this.data != null) {
while (this.data.length < this.minItemSize) {
this.push('data', {content: ""})
}
} else {
this.data = [{content: ""}];
}
this.reflectPropertyToAttribute('data');
}
});
</script>
</dom-module>
EDIT:
This is the live code: http://jsbin.com/poquke/1/edit?html,output

I have played around a bit with your code and I noticed that it will work if you wrap the code in your changed handler in an async function. This fixed both issues that you described.
_dataChanged: function (e) {
this.async(function(){
if (this.data != null) {
while (this.data.length < this.minItemSize) {
this.push('data', {content: ""})
}
} else {
this.data = [{content: ""}];
}
});
}
I don't have a perfect explanation for this behaviour. I assume it is related somehow to the way Polymer handles the observation for changes. Each time you push to the data array in the changed handler, this in fact changes data and should in turn trigger the handler again.

No async is required if you simplify.
Here is the simplified code, this removes the repeated calls to _dataChanged when you push the minimum values, and allows polymer's built-in eventing system to take care of updating and notifying the other elements. A function: _createNewItem() is for creating an object. This simplifies where item object creation is handled.
http://jsbin.com/vemita/6/edit?html,output
The link and URL references have changed from the sample code in the question above to conform to the polymer element and demo page standards to be used with polyserve.
I've commented on your original code for why each line should or shouldn't be there. this includes the reason for the changes to _dataChanged
http://jsbin.com/ponafoxade/1/edit?html,output

Related

can't use addEventListener() on Class [duplicate]

This question already has answers here:
What do querySelectorAll and getElementsBy* methods return?
(12 answers)
Closed 2 years ago.
Hi im trying to create a toDo list - when i press on the checkbox, the task completion should change to true. As there are several checkboxes, i have used the class element, however, when i try to add an event listener, it doesn't seem to let me. There are also large gaps between each text, the checkboxes were supposed to be there but i moved them, but the gaps still appear. Could someone please help. Thank you.
Im a noob at Javascript, and i wish to get better. Do you guys have tips on how to get better, also. Thank you.
const todos = [{
task: "Revision",
completed : false
},{
task: "Walk Dog",
completed : false
},{
task: "Read Book",
completed : false
},{
task: "Javascript Practice",
completed : false
},{
task: "VB.Net Practice",
completed : false
},{
task: "Gaming",
completed : false
}]
todos.forEach(function(todos) {
const list = document.createElement("h3")
list.textContent = `${todos.task} - ${todos.completed}`
document.getElementById("output").appendChild(list)
const button = document.createElement("input")
button.type = "checkbox"
button.setAttribute("class", "checkboxes")
document.getElementById("output").appendChild(button)
})
const complete = document.getElementsByClassName("checkboxes")
complete.addEventListener("click", function() {
todos.completed == true
})
body {
font-family: quicksand;
}
.checkboxes {
position: relative;
left: 250px;
bottom: 40px;
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>ToDo List</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>ToDo List</h1>
<div id="output">
</div>
<script src="todo.js"></script>
</body>
</html>
All of the comments point to valid problems with you code as it stands.
getElementsByClassName() returns a live HTMLCollection which you would need to iterate over in order to attach listeners to each element. If you are going to query I would recommend using querySelectorAll() which returns a static NodeList unless you really need the live reference to the DOM
todos.completed == true is a comparison rather than an assignment
But since you are creating the elements programmatically using document.createElement() you can save yourself the extra DOM query and iteration, and at the same time create a closure over the other elements you are creating in order to update them on event trigger by attaching the listener to the newly created element before appending it to the DOM.
The snippet below attaches a change listener, and instead of just setting completed to true, it toggles the value based on the checkbox's checked property.
const todos = [{ task: "Revision", completed: false }, { task: "Walk Dog", completed: false }, { task: "Read Book", completed: false }, { task: "Javascript Practice", completed: false }, { task: "VB.Net Practice", completed: false }, { task: "Gaming", completed: false }]
todos.forEach(function (todo) {
const list = document.createElement("h3")
list.textContent = `${todo.task} - ${todo.completed}`
document.getElementById("output").appendChild(list)
const button = document.createElement("input")
button.type = "checkbox"
button.setAttribute("class", "checkboxes")
// Add the event listener before appending.
button.addEventListener("change", function (e) {
todo.completed = e.target.checked;
list.textContent = `${todo.task} - ${todo.completed}`;
//console.log(todos);
})
document.getElementById("output").appendChild(button)
})
body {
font-family: quicksand;
}
.checkboxes {
position: relative;
left: 250px;
bottom: 40px;
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>ToDo List</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>ToDo List</h1>
<div id="output">
</div>
<script src="todo.js"></script>
</body>
</html>
EventTarget.addEventListener().
EventTarget is a DOM interface implemented by objects that can receive events and may have listeners for them. Element, Document, and Window are the most common event targets, but other objects can be event targets, too. For example XMLHttpRequest, AudioNode, AudioContext, and others. Many event targets (including elements, documents, and windows) also support setting event handlers via onevent properties and attributes.

How to generate Vue components from Javascript

I'm new to Vue.js (with a background in Computer Science and programming, including interactive Javascript webpages) and as I'm a teacher, I have a quiz site I use to give homework to my students.
My codebase is messy, so I decided to migrate the whole thing to Vue, with the idea that I could use a component for each individual type of question -- separation of concerns, and all that.
However, I can't seem to find a way to generate appropriate components on the fly and include them in my page.
Here's a simplified version of my framework, with two question types. If I include the components directly in the HTML, they work fine.
Vue.component("Freetext",{
props: ["prompt","solution"],
data : function() {return {
response:""
}},
methods : {
check : function () {
if (this.solution == this.response) {
alert ("Correct!");
app.nextQuestion();
} else {
alert ("Try again!");
}
}
},
template:'<span><h1>{{prompt}}</h1> <p><input type="text" v-model="response"></input></p> <p><button class="LG_checkbutton" #click="check()">Check</button></p></span>'
})
Vue.component("multi",{
props : { prompt: String,
options : Array,
key_index : Number // index of correct answer
},
data : function() {return {
response:""
}},
methods : {
check : function (k) {
if (k == this.key_index) {
alert ("Correct!");
app.nextQuestion();
} else {
alert ("Try again!");
}
}
},
template:'<span><h1>{{prompt}}</h1><button v-for="(v,k) in options" #click="check(k)">{{v}}</button></span>'
})
</script>
<div id="app">
<Freetext prompt="Type 'correct'." solution="correct"></freetext>
<multi prompt="Click the right answer." :options='["right","wrong","very wrong"]' :key_index=0></multi>
</div>
<script>
var app = new Vue({
el: "#app",
data : {
questions:[ {type:"Multi",
prompt: "Click the right answer.",
options:["right","wrong","very wrong"],
key:0},
{type:"Freetext",
prompt:"Type 'correct'.",
solution:"correct"}
],
question_number:0
},
methods : {
nextQuestion : function () {
this.question_number ++;
}
}
})
</script>
But what I want to do is generate the contents of the div app on the fly, based on using the data member app.question_number as an index to app.questions, and the .type member of the question indicated (i.e. app.questions[app.question_number].type)
If I try to make the app of the form:
{{question}}
</div>
<script>
//...
computed : {
question : function () {
var typ = this.questions[this.question_number].type;
return "<"+typ+"></"+typ+">";
}
...I just get as plain text, and it isn't parsed as HTML.
If I try document.getElementById("app").innerHTML = "<multi prompt='sdf'></multi>"; from the console, the tag shows up in the DOM inspector, and isn't processed by Vue, even if I call app.$forceUpdate().
Is there any way round this?
While Keith's answer works for most of what I need to do, there's another way to handle this that I've just found out about, which I thought I'd share in case anyone else is looking for it: giving a block level HTML element a v-html property.
For me, this is handy as a short term fix as I'm migrating a codebase that generates dynamic HTML as strings, and I can quickly integrate some of my existing code without reworking it completely.
For example, I have a function makeTimetable that takes a custom datastructure representing a week's actively and turns it into a table with days across the top and times down the left-hand side, setting appropriate rowspans for all the activities. (It's a bit of a convoluted function, but it does what I need and isn't really worth refactoring at this point.)
So I can use this as follows:
<script type="text/x-template" id="freetext-template">
<span>
<div v-html="tabulated_timetable"></div>
<p>{{prompt}}</p>
<p><input type="text" v-model="response"></input></p>
<p><button class="LG_checkbutton" #click="check()">Check</button></p>
</span>
</script>
<script>
var freetext = Vue.component("Freetext",{
props: {"prompt":String,
"timetable":Object,
"solution":String,
data : function() {return {
response:""
}},
computed : {
tabulated_timetable : function () {
return makeTimetable (this.timetable);
}},
methods : {
check : function () {
if (this.solution == this.response) {
alert ("Correct!");
app.nextQuestion();
} else {
alert ("Try again!");
}
}
},
template:'#freetext-template'
})
</script>
(I suppose I could put `tabulated_timetable` in `methods` rather than `computed`, as it's set once and never changed, but I don't know if there would be any performance benefit to doing it that way.)
I think maybe a slightly different approach, Vue supports the concept of "dynamic components"
see https://v2.vuejs.org/v2/guide/components-dynamic-async.html
this will let you define what component to use on each question which would look something like
<component v-bind:is="question.component" :question="question"></component>

Vue inline template not triggering method even with emit

I'm wondering what I am doing wrong here, from what I can see this is the solution: Vue: method is not a function within inline-template component tag
However the method is still not triggering.
<b-table
:items="filtered"
:fields="fields"
sort-icon-left
responsive="sm"
#card-click="setUpdate"
>
<template v-slot:head()="data">
<span class="text-info">{{ data.label.toUpperCase() }}</span>
<button #click="$emit('card-click', data)">filter</button>
<input
v-show="data.field.showFilters"
v-model="filters[data.field.key]"
:placeholder="data.label"
/>
</template>
</b-table>
methods: {
setUpdate(field) {
console.log("hello");
console.log(field);
this._originalField = Object.assign({}, field);
field.showFilters = true;
}
}
Update
So the #click allowed me to to trigger the event but this lead to the table wouldn't update with the changed data with showFilters. Thanks to MattBoothDev I found event-based-refreshing-of-data, however this oddly now prevents the data from changing. I.e. if field.showFilters is true it's true if I click the button.
methods: {
setUpdate(field) {
this._originalField = Object.assign({}, field);
field.showFilters = !field.showFilters;
this.refreshTable();
console.log(field.showFilters);
},
refreshTable() {
this.$root.$emit("bv::refresh::table", "my-table");
}
}
It looks like you're using Bootstrap Vue?
What you're essentially doing here is putting a listener on the <b-table> tag for card-click but that event is essentially not happening within a child component of <b-table>.
Regardless, I'm not even sure you need the event.
<button #click="$emit('card-click', data)">filter</button>
can easily just be
<button #click="setUpdate(data)">filter</button>
EDIT:
It is good practice to use MVVM for Vue.js as well.
Rather than: #click="$emit('card-click', data)"
Should be: #click="onFilterClicked"
Then:
methods: {
onFilterClicked (data) {
this.$emit('an-event', data.some.property)
}
}
This will make testing your code a lot easier.

vue.js computed beginner

I am trying to learn vue.js despite not having any background with javascript. I ran into some code when following a video that was teaching about 'computed', and I tried experimenting on it and had a bit of trouble along the way.
<div id='app'>
<p>Do you see me?</p>
<p v-if="show">Do you also see me?</p>
<button #click="showToggle1">Switch!</button>
</div>
new Vue({
el:'#app',
data:{
show = true;
},
computed:{
showToggle1:function(){
return this.show = !this.show
}
},
methods:{
showToggle2:function(){
this.show = !this.show;
}
});
Basically it's making "Do you also see me?" disappear and appear depending on the value of "show". I know that if you write #click:'showToggle2()' instead of #click:'showToggle1' at the button, the value changes and it works. I'm just having some trouble understanding how computed works and why showToggle1 doesn't change the value of show when I click the button
Some problems.
First, you have syntactical problems. data is an object, so instead of:
data:{
show = true;
}
Should be:
data:{
show: true
}
Next, computed properties are to be used like... properties. For example, like declared in data. So, typicall, you read from them. You don't execute computed properties in #click events. So this code:
<button #click="showToggle1">Switch!</button>
Is not correct. It will error because showToggle1 is not a method, it is, as said, a computed property. What you should have in click is a method, like:
<button #click="showToggle2">Switch!</button>
This will work because showToggle2 is a method. And you should use methods to perform changes.
Not, before going into the last and most tricky part, here's a working demo:
new Vue({
el: '#app',
data: {
show: true
},
computed: {
/*showToggle1: function() {
return this.show = !this.show;
}*/
},
methods: {
showToggle2: function() {
this.show = !this.show;
}
}
});
<script src="https://unpkg.com/vue"></script>
<div id='app'>
<p>Do you see me?</p>
<p v-if="show">Do you also see me?</p>
<hr>
Value of show: {{ show }}<br>
<button #click="showToggle2">Switch2!</button>
</div>
The tricky part is your computed property (which I commented out in the code above):
computed:{
showToggle1:function(){
return this.show = !this.show
}
},
Basically what it is doing is it is automatically negating the value of show whenever it changes.
This happens because the computed property is calculated whenever show updates. And what is happening is:
You initialize data with true (because of data: {show: true}).
The showToggle1 computed auto-recalculates, because it has this.show inside of it (it depends on it).
When recalculating, showToggle1 sets the value of show to false (because of return this.show = !this.show).
That's why show becomes false.
And that's also why whenever you change (even from the method, which is the correct place) the value of show to true it will automatically go back to false. Because any change in show triggers the showToggle1 computed recalculation, which sets show back to false.
In summary:
Use methods to perform changes.
Don't change properties inside computed properties.

Handle Keyboard events for iron-list in GWT?

I use iron-list from google Polymer.
<iron-list items="[[data]]" as="item">
<template>
<div tabindex$="[[tabIndex]]">
Name: [[item.name]]
</div>
</template>
</iron-list>
I kwon you can use Polymer.IronA11yKeysBehavior but even with the example I have no idea how I add it in JavaScript to my iron-list.
Using Vaadin Polymer GWT lib. In this lib you have
IronList list;
list.setKeyBindings(???); // don't know how to use this function
list.setKeyEventTarget(????); // don't know how to use this function
When I check the current values of the key bindings I defined a print function to log a variable to the console:
public native void print(JavaScriptObject obj) /-{
console.log(obj);
}-/;
Then I print the current values with:
print(list.getKeyBindings());
The result is:
Object {up: "_didMoveUp", down: "_didMoveDown", enter: "_didEnter"}
It seem that there are some key bindings already defined, but I have no idea where I find the functions _didMoveUp, _didMoveDown and _didEnter.
When I do
print(list.getKeyEventTarget());
I get:
<iron-list class="fit x-scope iron-list-0" tabindex="1" style="overflow: auto;">
</iron-list>
How can I set up a handler for capturing keyboard events using Vaadin Polymer GWT lib? How can I receive an event when keys like enter are pressed?
answering this question
list.setKeyBindings(???); // don't know how to use this function
according to com/vaadin/polymer/vaadin-gwt-polymer-elements/1.2.3.1-SNAPSHOT/vaadin-gwt-polymer-elements-1.2.3.1-20160201.114641-2.jar!/com/vaadin/polymer/public/bower_components/iron-list/iron-list.html:292
the keyBindings should have object of such structure:
{
'up': '_didMoveUp',
'down': '_didMoveDown',
'enter': '_didEnter'
}
to construct such object, you can use following:
new JSONObject() {{
put("up", new JSONString("_didMoveUp"));
put("down", new JSONString("_didMoveDown"));
put("enter", new JSONString("_didEnter"));
}}.getJavaScriptObject();
I have no idea where I find the functions _didMoveUp, _didMoveDown and
_didEnter
they can be found here: com/vaadin/polymer/vaadin-gwt-polymer-elements/1.2.3.1-SNAPSHOT/vaadin-gwt-polymer-elements-1.2.3.1-20160201.114641-2.jar!/com/vaadin/polymer/public/bower_components/iron-list/iron-list.html:1504
here's the extract
_didMoveUp: function() {
this._focusPhysicalItem(Math.max(0, this._focusedIndex - 1));
},
_didMoveDown: function() {
this._focusPhysicalItem(Math.min(this._virtualCount, this._focusedIndex + 1));
},
_didEnter: function(e) {
// focus the currently focused physical item
this._focusPhysicalItem(this._focusedIndex);
// toggle selection
this._selectionHandler(e.detail.keyboardEvent);
}
How can I set up a handler for capturing keyboard events using Vaadin
Polymer GWT lib?
How can I receive an event when keys like enter are
pressed?
I could find this Polymer convention: properties not intended for external use should be prefixed with an underscore.
That's the reason why they are not exposed in JsType IronListElement.
You can change this function using JSNI. I think that smth like this:
private native static void changeDidMoveUp(IronListElement ironList) /*-{
var _old = ironList._didMoveUp;
ironList._didMoveUp = function() {
console.log('tracking');
_old();
}
}-*/;
or add a new one
IronListElement element ...
com.vaadin.polymer.elemental.Function<Void, Event> func = event -> {
logger.debug(event.detail);
...
return null;
};
private native static void addUpdatePressed(IronListElement ironList, Function func) /*-{
ironList._updatePressed = func;
}-*/;
{
addUpdatePressed(element, func);
element.addOwnKeyBinding("a", "_updatePressed");
element.addOwnKeyBinding("shift+a alt+a", "_updatePressed");
element.addOwnKeyBinding("shift+tab shift+space", "_updatePressed");
}
should work. You can get element from IronList#getPolymerElement().
keep in mind, that I haven't tested this code :)
If you want to add key event custom element (I dont know if I understand correct the question)
You have to implement the Polymer.IronA11yKeysBehavior behavior and then to use the keyBindings prototype property to express what combination of keys will trigger the event to fire.
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<base href="https://polygit.org/components/">
<script src="webcomponentsjs/webcomponents.js"></script>
<link rel="import" href="iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
<link rel="import" href="iron-list/iron-list.html">
<body>
<dom-module id="x-elem">
<template>
<iron-list items="[[data]]" as="item">
<template>
<div>
pressed: [[item]]
</div>
</template>
</template>
</dom-module>
<script>
Polymer({
is: 'x-elem',
behaviors: [
Polymer.IronA11yKeysBehavior
],
properties: {
preventDefault:{type:Boolean,value:true},
data:{value:[]},
keyEventTarget: {
type: Object,
value: function() {
return document.body;
}
}
},
keyBindings: {
'* pageup pagedown left right down up home end space enter # ~ " $ ? ! \\ + : # backspace': '_handler',
'a': '_handler',
'shift+a alt+a': '_handler',
'shift+tab shift+space': '_handler'
},
_handler: function(event) {
if (this.preventDefault) { // try to set true/false and press shit+a and press up or down
event.preventDefault();
}
console.log(event.detail.combo);
this.push('data',event.detail.combo);
}
});
</script>
<x-elem></x-elem>
</body>
</html>
I hope that answer your question how to listen to keys. the _handler function receive an event so you can look at the detail of the event and get the target (if something was in focus).
event.detail.target

Categories

Resources