How is it possible to show changes of existing items in a dom-repeat template in polymer?
i tried really all i could think of and i could find in the polymer documentation or in the web. but nothing works. below you find a html-page that uses a small list and tries to change one entry in the list to another value when you click the change button. But the only thing that would change the item in the list next to the change-button is the line that is commented out. all the other lines try it, but fail.
i understand that re-rendering a template is a time-consuming task and that it only should take place when it is necessary and that polymer tries to avoid it as much as possible. but why is it not possible for me (from the view of the code the god of their world ^^) to force a render on purpose?
the method of creating a complete new object, deleting the old item from the list and inserting the new object (thats what the commented line does) is a workaround, but it is a really huge effort, when the items are more complex and have properties or arrays that are not even displayed.
What am i missing? What did i not try? I would be very glad if anybody could tell me what i could do to achieve such a (from my point of view) simple and very common task.
EDIT (solved):
the solution of Tomasz Pluskiewicz was partly successful. but i updated the code to show my current problem. the name of the item is bound using the method format(...). when a button is clicked the item will be removed from the list. this works good. but if you remove the last item of the list, then the new last item in the list should get the name "Last". this also works, when the name is bound directly to the property name. but if i want to do some formatting of the name (surround it with # for example) then the display of this item is not updated.
EDIT2 (partially solved):
The next example that doesn't work, occurs when a value inside the method that is called for displaying a value changes. This can be seen in the example if a change-button is clicked multiple times. It increases an internal counter and the corresponding text will display this value. But this is only true for the first change of the counter. Subsequent clicks won't change the display again. The display shows the value of the counter after the first click. But if another change button is clicked, then the text in front of this button shows the increased counter value. But also only once. It also doesn't display changes on subsequent clicks. The notifyPath-method seems to check if the value changed, but doesn't consider that inside the method that is used for displaying the value, something could have been changed to show the data in another way.
i included a partial solution in this example. If the method that gets called has a parameter that changes when something in the method is changed, then the update will be executed. This can be seen in the second variable that is bound with the parameter displayEnforcer - format(item.name,displayEnforcer). This variable is set to a random value everytime the counter is changed. And this triggers the display update.
But this is a really strange solution and should not be necessary. it would be great if someone has a better solution to this problem.
<link rel="import" href="components/bower_components/polymer/polymer.html">
<link rel="import" href="components/bower_components/paper-button/paper-button.html">
<dom-module id="polymer-test">
<template>
<table>
<template id="tpl" is="dom-repeat" items="{{list}}">
<tr>
<td>{{item.id}} - {{format(item.name)}}- {{format(item.name,displayEnforcer)}}</td>
<td><paper-button raised on-tap="tapDelete">delete</paper-button></td>
<td><paper-button raised on-tap="tapChange">change</paper-button></td>
</tr>
</template>
</table>
</template>
</dom-module>
<script>
Polymer(
{
is: "polymer-test",
properties:
{
count: {type: Number, value:0}
,list: {type: Array, value: [{id:0,name:"First"}
,{id:1,name:"Second"}
,{id:2,name:"Third"}
,{id:3,name:"Fourth"}
,{id:4,name:"Fifth"}
,{id:5,name:"Last"}
]}
,displayEnforcer: {type:Number,value:Math.random()}
},
format: function(name,dummy)
{
return "#" + name + " - " + this.count + "#";
},
tapChange: function(e)
{
this.count++;
this.displayEnforcer = Math.random();
this.notifyPath("list." + (e.model.index) + ".name","changed");
},
tapDelete: function(e)
{
if(this.list.length == 1)
return;
this.splice("list",e.model.index,1);
if(this.list.length > 0)
this.list[this.list.length-1].name = "Last";
this.notifyPath("list." + (this.list.length-1) + ".name",this.list[this.list.length-1].name);
}
});
</script>
You can use notifyPath to refresh binding of single list element's property:
tapChange: function(e) {
this.notifyPath('list.' + e.model.index + '.name', 'changed');
}
See: https://github.com/Polymer/polymer/issues/2068#issuecomment-120767748
This way does not re-render the entire repeater but simply updates the necessary data-bound nodes.
EDIT
Your format function is not getting updated because it doesn't use the notified path which is an item's name. When you remove an element, the index of that element within the rendered repeater doesn't change, even though the actual item changes. In other words, when you remove fifth element, the index 4 is still 4.
As a workaround you can add item.name to format call so that it is refreshed when you notify that path:
<td>{{item.id}} - {{format(index, item.name)}}</td>
You don't even have to use that value in the example. It's enough that the binding is reevaluated.
Related
Using Haml, ruby 2.7.2, rails 6.1.2.1, stimulus ^2.0.0
So, I am using a nested form that shows a select type. Based on the value of the select type, I will either show or hide a div. I'm converting my coffeescript to stimulus, and I'm not sure how to force trigger a change event, or instead just run the code that checks the value of the select to decide if I should hide or show the div. In the past, Inside my html.haml view I would call this:
:javascript
$('select.answer_type').change()
Based on another post in here: https://discuss.hotwire.dev/t/triggering-turbo-frame-with-js/1622, I tried to change it to say:
:javascript
q_typeTarget.dispatchEvent(new Event('change'));
And that didn't work. Maybe there is another way to do this, or I'm using the wrong vocabulary to look up the example I want to do? I'd rather just manually execute the Controller#action and give it the select object to work off of right away, but I'm not sure how to do this in html/script tag. Thanks for any help!
Here is my relevant code that should help:
_question_fields.html.haml
.nested-fields.question
.form_group.grid-x
.fields.cell.shrink
= f.select :answer_type,
options_for_select(["String","Option","Checkbox"],
f.object.answer_type),{},
class: 'answer_type',
data: {nested_form_target: 'q_type',
action: 'nested-form#update_q_type'}
.cell.answers_group{ style: 'visibility: hidden; display: none'}
= render partial: 'answers', locals: { f: f, q_level: q_level }
:javascript
q_typeTarget.dispatchEvent(new Event('change'));
nested_form_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
static targets = ["q_type", "answers"]
update_q_type(event) {
event.preventDefault()
console.log('You have selected ' + this.q_type )
let d_question = event.target.closest(".question")
let d_select = d_question.querySelector("select")
let d_answer = d_question.querySelector(".answers_group")
console.log('Select: ' + d_select.value )
switch(d_select.value) {
case 'Option':
case 'Checkbox':
console.log(' Show Answers ')
d_answer.style.visibility = "visible"
d_answer.style.display = null
return
default:
console.log(' Hide Answers ')
d_answer.style.visibility = 'hidden'
d_answer.style.display = 'none'
return
}
}
Here is an example .gif of the nested forms, shown below:
I add a Question
I select an option from the select tag, which triggers the select → change event: nested-form#update_q_type
If I select either Option or Checkbox, then I show the div, otherwise I hide the div
Now, here is why I'm trying to trigger the select → change event. If I start to edit this record again, it loads the data just fine. However, it does not know when to show the answers div or not. That's why I need to trigger the select → change event so that it can properly set up the answers div to either hide or show. Otherwise, it's defaulted to always 'hide'.
I have another .gif below. As you can see, it loads the select with Option selected. if I try to select Option again, it doesn't do anything...because it hasn't really changed. If I change it to Checkbox, it of course updates. So basically, I need to do this onLoad, you can say.
Maybe there is a different way to do this? Thank you for your help!
Update: I didn't realize my html tags were being interpreted as html. Oops. Changed all tags to italicized words. Ex: '/</select/>/' is now just select. Also I expanded on the question for more clarification.
StimulusJS has an initialize function that essentially runs when the page loads. You would use that for this purpose. The only other issue here is that you're passing in an event to update_q_type and you won't have an event on initialize. So what I find works is to extract most of the code to a separate function that my initialize and click function can both call. This helps you keep your code DRY.
export default class extends Controller {
static targets = ["q_type", "answers"]
initialize() {
this.switch(this.q_typeTarget)
}
update_q_type(event) {
event.preventDefault
this.switch(this.q_typeTarget)
}
switch(q_type) {
...
}
}
One thing to note, it's not clear if you have one stimulus controller running here or one for every select box combo. You could do the 2nd and not have to use the closest method. It's meant to be very modular.
I am forcing element to be focused like this
/**focusing the element if the element is active */
componentDidUpdate() {
console.log(this.activeElementContainer);
if(this.activeElementContainer!==undefined && this.activeElementContainer!==null) {
/** need to focus the active elemnent for the keyboard bindings */
this.activeElementContainer.focus();
}
}
My render has conditional rendering the elements are being rendered dynamically from the array,
Let say I have one element in div and I am adding another from the toolbox. In that case I need to focus the last element I dragged.
render() {
let childControl= <span tabIndex="-1" dangerouslySetInnerHTML={{__html: htmlToAdd}}></span>;
if(this.props.activeItem){
childControl=<span ref={ (c) => this.activeElementContainer = c }
tabIndex="0" dangerouslySetInnerHTML={{__html: htmlToAdd}}></span>
}
//later I ma using childControl to array and it works fine.
The logs says, first time it works fine
But, second time the this.activeElementContainer is undefined
Is there any alternative way or possible solution to this?
The thing is I need to focus only one element at the time.
Remember: Activecontrol has too many things to do like it can have right click menu, drag etc. so, I need to render it separately.
After reading this one github:
This is intended (discussed elsewhere) but rather unintuitive
behavior. Every time you render:
<Value ref={(e) => { if (e) { console.log("ref", e); }}} /> You are
generating a new function and supplying it as the ref-callback. React
has no way of knowing that it's (for all intents and purposes)
identical to the previous one so React treats the new ref callback as
different from the previous one and initializes it with the current
reference.
PS. Blame JavaScript :P
Source
I changed my code to
<span id={this.props.uniqueId} ref={(c)=>
{if (c) { this.activeElementContainer=c; }}
} tabIndex="0" dangerouslySetInnerHTML={{__html: htmlToAdd}}></span>
Adding if was the real change. now, it has a ref.
For others who face this problem:
I need to write custom function too, in the componentDidUpdate I am still getting old reference,
ref={(c)=>{if (c) { this.activeElementContainer=c; this.ChangeFocus(); }}
Adding this was the perfect solution for me
Here is the bug
I loop the input files with each container. Then set the background image from the file input of its children.
<div class="thumbnailHolder" *ngFor="let colImage of proPub.image ;let y = index trackBy: trackByFn" [attr.data-index]="y">
<span class="removeImage" (click)="deleteNewImageColour($event,i,y)">X</span>
<input
class="imaged" type="file" name="" required="required"
[(ngModel)]="productPublish[i].image[y]" (change)="fileChangeEvent($event, i, y)"
/>
</div>
If I have ten images which are pushed to an array when added, the fileChangeEvent will set the background image to its container.
reader.onload = function (e) {
imageSrc = e.target['result'];
fileInput.srcElement.parentNode.style.backgroundImage = 'url(' + imageSrc + ')';
};
However, if I delete an array, let's say: indexOf 9,
image.splice(y, 1)
It will mess up the loop for the background image. The background image is not set for its correct input file as expected.
And if I delete it by removing the source element, it won't delete the array.
srcElement.parentElement.remove()
It works fine if I combine the two methods. But it only deletes the last array or element. If I do it with any array besides the last array then it will delete two things simultaneously(at the same time), first the array and then the element of which will delete the loop two times.
Can anybody help, please?
https://stackblitz.com/edit/angular-g5pgiy Hope that fixes your problem.
I took liberty to remove some non-essential things like data-index attributes.
Angular abstracts away DOM modifications so you shouldn't modify nodes directly. Otherwise results are random-ish like in your example.
Moreover, using trackBy isn't meant to be used like that. It's optimization technique to prevent superfluous rendering of unchanged elements. Your implementation says that "array elements never change indices" and later you change them by using splice.
How do I hide a single tab on a MVC/Kendo UI tabstrip?
I want to hide a tab based on a condition. My jQuery code goes like this:
//authUser is a hidden input whose value is set in the controller and passed into the view
if ($('#authUser').val() == 'False') //hide the last tab
{
$($("#tabstrip").data("kendoTabStrip").items()[6]).attr("style", "display:none");
}
When I run the code I get the following error on the line of code that's executed if authUser is False:
JavaScript runtime error: Unable to get property 'items' of undefined or null reference
Ideas?
The fact that 'items' is undefined implies that you never appropriately selected the tabstrip in the first place. Either your CSS selector is wrong (are you sure you named it tabstrip?) or you did not follow the Kendo method names appropriately.
Here are two ways I found to hide the last tab:
Hiding the last tabstrip element
var tabStrip = $("#tabstrip").kendoTabStrip().data("kendoTabStrip");
//Find the last tab item's index from the items list
var lastIndex = tabStrip.items().length - 1;
//Use jQuery's hide method on the element
$(tabStrip.items()[lastIndex]).hide();
Using Kendo's tabstrip remove method
I believe the following is more appropriate. Why not use the tabstrip's remove method and completely remove it from the DOM since the user should not have access anyway?
var tabStrip = $("#tabstrip").kendoTabStrip().data("kendoTabStrip");
tabStrip.remove("li:last");
I'm just stupid....
I looked some more at the code and I was leaving out the kendoTabStrip() word (bolded) from
$($("#tabstrip").kendoTabstrip().data("kendoTabStrip").items()[6]).attr("style","display:none")
i.e. Instead of (properly) having:
$($("#tabstrip").kendoTabStrip().data("kendoTabStrip").items()[6]).attr("style","display:none")
I had:
$($("#tabstrip").data("kendoTabStrip").items()[6]).attr("style","display:none")
Drew, thanks for your effort. Sometimes I just have to beat my head on a wall until I see what I've done.
I've a page with about 10 short articles.
Each of them as a "Read More" button which when pressed displays hidden text
The issues I have at the moment is when I press the "Read More" on any of the 10 button it shows the 1st articles hidden content and not the selected one.
I think I need to set a unique ID to each article.. and the read more button be linked to it.. But I don't know how to set it.
I looked at this but couldn't get it working how to give a div tag a unique id using javascript
var WidgetContentHideDisplay = {
init:function() {
if ($('#content-display-hide').size() == 0) return;
$('.triggerable').click(function(e){
var element_id = $(this).attr('rel');
var element = $('#'+element_id);
element.toggle();
if (element.is(':visible')) {
$('.readmore').hide();
} else {
$('.readmore').show();
}
return false;
});
}
}
var div = documentElemnt("div");
div.id = "div_" + new Date().gettime().toString;
$(document).ready(function(){ WidgetContentHideDisplay.init(); });
OP Edit: Sorry, the original code wasn't in caps. I kept getting errors when trying to post, so I copied the code into Dreamweaver and it made it all caps for some reason.
Instead of selecting the element to toggle with an ID (i.e. $('#'+ELEMENT_ID)) you could setup a class for your item and use the class selection (e.g. $('.DETAILED-ARTICLE)') to select the child (or the brother, etc. depending how you built the HTML page).
In theory each ID should point to a single element but each class can be put to as many elements as you want.
If you're getting errors, read the errors and see what they are. Off of a quick read of your code, here are a couple things I noticed that will probably cause issues:
"documentElemnt" is misspelled, which will render it useless. Also, documentElement is a read-only property, not a function like you're using it.
toString is a function, not a property, without the parentheses (.toString()) it isn't going to function like you want it to.
Run the code, look at the errors in the console, and fix them. That's where you start.