QBO V3 Behavior order of operations - javascript

I'm running into a problem creating a "depends" behavior where the object to depend on is an <option/> tag that has not loaded up yet because it loads up via an ajax call. There appears to be a race condition where the depends behavior is trying to load up before the ajax call is complete. Here is the element markup that failing to load the depends behavior:
<div class="control-group" data-behavior="Depend" data-depend-options="{{'depends': 'W9', 'required': true}}">
"W9" will be the ID of the <option/> tag. This option tag will be loaded up by the following code:
<select name="ProfessionalLicenseType" id="ProfessionalLicenseType" class="required" data-behavior="Dropdown" data-dropdown-options="{{ 'type': 'ObjectType', 'data': {{ 'Object': 'ProfessionalLicense' }}, 'selected': '{AttachmentType}', 'id': 'ObjectType' }}">
Should I not use the HTML markup to create the dependency behavior? Should I instead try to use javascript to create this dependency?
Thanks in advance.

The Depend behavior support binding to a select tag's value, rather than an option tag:
<div class="control-group" data-behavior="Depend" data-depend-options="{{'depends': 'ProfessionalLicenseType=W9', 'required': true}}">
The Depend behavior, on initialization, will check the value of ProfessionalLicenseType, and find that it's empty, disabling/hiding your dependent div tag. It will also add a change event handler to the ProfessionalLicenseType dropdown (qbo.Depends.js line 45):
source.addEvent('change', qbo3.dependencyCheck.pass([source, element, options, depends]));
The Dropdown behavior will later (asynchronously) load your options via AJAX, and if the behavior's options.selected is set, will set the matching option.selected=true (qbo.Dropdown.js line 105):
if ((row[value] || row) == options.selected) {
target.options[target.options.length - 1].selected = true;
target.defaultIndex = target.options.length - 1;
target.fireEvent('change');
}
The to this working is the target.fireEvent('change') noted above; that will trigger the Depend behavior to re-evaluate the dependency and react appropriately.

Related

Manually force trigger a change event on select in nested form using stimulusjs

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.

How can i force a template-update in Polymer?

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.

jQuery not recognizing dynamically-inserted DOM element?

Yet another "dynamically change select options based on parent option selected" question.
I have the select values changing dynamically - but after my rails render of the child select - I lose the Chosen styling (the jQuery Chosen plugin) and I cannot operate on this newly injected element.
Here is where the code is right now - it's gone through dozens of iterations -
$('#vendor_block').on('change', '#vendor_name', function(){
overlay.show();
var v = $(this).val();
vndr_json = {};
vndr_json["v"] = v;
$.ajax({
type : "GET",
url : "/purchaseorders/upd_vndr_locs",
data : vndr_json,
success: function(res) {
overlay.hide();
// console.log(typeof(res),res);
jQuery("#vndrAddrOpts").html(res);
}
});
$("#vendor_addresses").chosen(); // WHY DON'T YOU RENDER CHOSEN BOX?!
});
I get this new select box on my page - and I want to fire an event when it changes, but the DOM has already loaded, so it doesn't "see" this element I'm guessing.
Also - the Chosen plugin doesn't render on the element. Not sure why - probably the same reason.
I'm using jQuery's .on() like every post on SO says I should. But it doesn't "reload" the elements inside this parent (and 'vendor_block' is the parent div of 'vendor_name' and 'vendor_addresses').
You can see the difference in the select boxes here:
Any help would be great?
UPDATE:
Adding before and after HTML :
<div id="vndrAddrOpts">
<select class="chzn-select vndrLocs span12" id="vendor_addresses" name="vendor_addresses"><option value="">Select Location</option></select>
</div>
That is the raw HTML - but Chosen does the following when the DOM loads:
<div id="vendor_addresses_chzn" class="chzn-container chzn-container-single chzn-with-drop chzn-container-active" style="width: 100%; margin-bottom: 10px;" title=""><span>Select Location</span><div><b></b></div><div class="chzn-drop"><div class="chzn-search"><input type="text" autocomplete="off"></div><ul class="chzn-results"><li id="vendor_addresses_chzn_o_0" class="active-result result-selected highlighted" style="">Select Location</li></ul></div></div>
This is all fine and well - this is what's supposed to happen.
This is the raw HTML after the select box has been injected:
<div id="vndrAddrOpts">
<select class="chzn-select vndrLocs span12" id="vendor_addresses" name="vendor_addresses"><option value="">Select Location</option></select>
</div>
And here is the rendered box - sans Chosen stuff.
<select class="chzn-select vndrLocs span12" id="vendor_addresses" name="vendor_addresses"><option value="">Select Location</option><option value="532757b4963e6505bc000003">Honolulu</option>
<option value="532768d0963e6505bc000004">Waipahu</option></select>
I found the answer here :
Is there a way to dynamically ajax add elements through jquery chosen plugin?
I actually was approaching this problem in an overly complex way - trying to inject and element instead of just starting with the element and adding options to it.
My AJAX looks like this now:
$.ajax({
type : "GET",
url : "/purchaseorders/upd_vndr_locs",
data : vndr_json,
success: function(res) {
overlay.hide();
var va = $('#vendor_addresses');
// console.log(typeof(res),res);
for (var i=0; i < res.length; i++) {
va.append(
$('<option></option>')
.val(res[i].id)
.html(res[i].name)
);
}
va.trigger("liszt:updated");
// jQuery("#vndrAddrOpts").html(res);
}
});
So instead of even worrying about rebuilding the chosen element from an injected element - we just use the built-in "updated" trigger and it works great.
You are inserting the result of your ajax call into the DOM it's success callback, which is executed whenever it finishes (independent of the script's execution). In this case, your ajax request is being made, the code after it begins executing, and then the callback. The odds of the success callback being called before the next line of code are slim, as the ajax call is an http request which takes much longer than a line of JavaScript executing.
You want to put the code in the success call back, such as:
$.ajax({
type : "GET",
url : "/purchaseorders/upd_vndr_locs",
data : vndr_json,
success: function(res) {
overlay.hide();
$("#vndrAddrOpts").html(res);
$("#vendor_addresses").chosen();
}
});
I think the chosen method is firing before the element has actually been rendered on the page, and jQuery can't find it. Try putting $("#vendor_addresses").chosen(); as part of the AJAX success callback. Failing that, try commenting out the chosen() method, running the AJAX script, then manually running the chosen() method. If it works that way, you have to delay it a little bit.
EDIT:
Actually, looking more closely at your code, it appears you're using an ID tag instead of a class. Do multiple HTML elements have the #vendor_address id? If so, use a class instead, and use $('.vendor_addresses').last().chosen();. If you use an ID, and use an ID selector, jQuery will pick the first element if finds with that ID, and stop there.
Lesson to be learned? Use an ID for UNIQUE elements, and classes for multiple elements of the same 'class'.

HTML 5 pattern attribute

Is there a way in <select> list, for example, to make onClick activate a JavaScript function that will show the title of the element just as pattern in HTML 5 does?
I want to do that when you click on the <select>, it will activate a JavaScript function that under some condition (doesn’t matter—some if expression) will show a sentence (that I wrote) in a bubble like place (the place that the pattern shows the title when something isn’t according to the pattern (pattern in HTML5)).
You can set a custom validity error on a select element by calling the setCustomValidity method, which is part of the constraint validation API in HTML5 CR. This should cause an error to be reported, upon an attempt at submitting the form, in a manner similar to reporting pattern mismatches. Example:
<select onclick="this.setCustomValidity('Error in selection');
title="Select a good option">
(In practice, you would probably not want to use onclick but onchange. But the question specifically mentions onClick.)
There are problems, though. This only sets an error condition and makes the element match the :invalid selector, so some error indicator may happen, but the error message is displayed only when the form data is being validated due to clicking on a submit button or something similar. In theory, you could use the reportValidity method to have the error shown immediately, but browsers don’t support it yet.
On Firefox, the width of the “bubble” is limited by the width of the select element and may become badly truncated if the longest option text is short. There is a simple CSS cure to that (though with a possible impact on the select menu appearance of course).
select { min-width: 150px }
You might also consider the following alternative, which does not affect the select element appearance in the normal state but may cause it to become wider when you set the custom error:
select:invalid { min-width: 150px }
There is also the problem that Firefox does not include the title attribute value in the bubble. A possible workaround (which may or may not be feasible, depending on context) is to omit the title attribute and include all the text needed into the argument that you pass to setCustomValidity.
A possible use case that I can imagine is a form with a select menu such that some options there are not allowed depending on the user’s previous choices. Then you could have
<select onchange="if(notAllowed(this)) setCustomValidity('Selection not allowed')" ...>
where notAllowed() is a suitable testing function that you define. However, it is probably better usability to either remove or disable options in a select as soon as some user’s choices make them disallowed. Admittedly, it might mean more coding work (especially since you would need to undo that if the user changes the other data so that the options should become allowed again).
In my opinion Jukka's solution is superior however, its fairly trivial to do something approaching what you're asking for in JavaScript. I've created a rudimentary script and example jsFiddle which should be enough to get you going.
var SelectBoxTip = {
init : function(){
SelectBoxTip.createTip();
SelectBoxTip.addListeners();
},
addListeners : function(){
var selects = document.getElementsByTagName("select");
for (var i = 0; i < selects.length; i++){
var zis = selects[i];
if(zis.getAttribute('title')){//only if it has a title
zis.addEventListener("focus", SelectBoxTip.showTip, false);
zis.addEventListener("blur", SelectBoxTip.hideTip, false);
}
}
},
createTip : function(){
tip = document.createElement("div");
tip.id = "tip";
tip.style.position = "absolute";
tip.style.bottom = "100%";
tip.style.left = "0";
tip.style.backgroundColor = "yellow";
document.body.appendChild(tip);
},
showTip : function(e){
this.parentNode.appendChild(tip);
tip.innerHTML=this.title;
tip.style.display="block";
},
hideTip : function(e){
tip.style.display="none";
}
};
SelectBoxTip.init();

selectmenu ('refresh', true)

I get form from zend framework site and put it in response in new file in function written by jquery mobile, but I get this error:
uncaught exception: cannot call methods on selectmenu prior to
initialization; attempted to call method 'refresh' .
Code of function this file:
function addItem(id) {
$.ajax({
url:'http://zf.darina.php.nixsolutions.com/order/index/create-order-mobile',
dataType:"jsonp",
data:{id_good:id},
success:function (resp) {
console.log(resp);
$('.product-table').empty();
$('.product-table').append(resp.prod);
$('.product-table').append(resp.form);
$('.add-order-btn').button();
$('.mob-size').selectmenu('refresh', true);
$('#block').page();
}
})
}
Force initialize the selectmenu(s) first:
$('.mob-size').selectmenu(); // Initializes
$('.mob-size').selectmenu('refresh', true);
or use this for short
$('.mob-size').selectmenu().selectmenu('refresh', true);
In my case, if I was not initializing the select before invoking the 'disable' method I got the error, while if I was initializing it, the select didn't disable but duplicate itself - I tried to select the object by TAG NAME instead of by CLASS or ID NAME,
$('select').selectmenu('disable');
instead of
$('.select-class-name').selectmenu('disable');
and it worked without forced initialization
you do this in your custom refresh delegation function:
var w = $("#yourinput");
if( w.data("mobile-selectmenu") === undefined) {
// not initialized yet, lets do so
w.selectmenu();
}
w.selectmenu("refresh",true);
according to enhancement resolution here
I found the same problem, but a more involved solution. When jqm wraps the select element, it puts it in a <span> element with the same class list as the select element. I changed my reference to it so that instead of reading:
row.find(".selectCompletion").selectmenu("disable");
it now reads:
row.find("select.selectCompletion").selectmenu("disable");
Specifying that jquery should only find the select element matching the class name, rather than all elements in .row that match the class name, solved the problem.
This happened to me when cloning existing select element in order to duplicate the original section multiple times.
Calling 'refresh' for the original element, worked fine, while calling it for the cloned sections was leading to the error appearing in the question.
However, calling selectmenu() was causing a 'vandalisation' to the form, as can be seen in the following image:
Explanation: top = original. bottom = vandalised cloned element right after calling selectmenu.
Solution:
The following code solved this vandalisation problem:
cloned_elem.find('select[name=MyClass]').selectmenu().selectmenu("destroy").selectmenu();
This is not an ideal solution because we must call the first selectmenu() in order to call selectmenu("destroy"), so I would be glad to hear of a cleaner solution.

Categories