I need to get the Element DOM from an observable object.
Please look on this example.
I'm clicking on the button "Get Class" and then I want to get valueB DOM element.
Please pay attention that I want valueB DOM element and not buttonA
Example:
HTML
<button id="buttonA" data-bind="event:{click: getClassFromValueB}">Get Class</button>
<input id="valueB" class="Hello" data-bind="value: observables.idNumber"/>
VIEWMODEL
"
"
"
observables : {
idNumber: ko.observable('SomeText');
},
getClassFromValueB : function(child, event){
idNumber_DOM = this.getElementDOM(this.observables.idNumber);
},
getElementDOM : function(observable){
**//WHAT TO DO HERE????**
}
"
"
"
I seeking a solution without jQuery...
$(event.target).closest('#valueB')
UPDATE: The main reason for this question is to to clear a customized attribute in a input when one of the other inputs are change
Example:
<input id="InputA" class="Hello" data-bind="event:{change: clearInputB}"/>
<input id="InputB" class="Hello" data-bind="value: observables.idNumber"/>
<input id="InputC" class="Hello" data-bind="event:{change: clearInputB}"/>
<input id="InputD" class="Hello" data-bind="event:{change: clearInputB}"/>
Not exactly what you're asking for but maybe you can solve the problem in a different way. If you're after a DOM element's class, Knockout provides a css binding. valueB's data-bind statement would look something like this:
<input id="valueB" data-bind="css: observables.valueBClass, value: observables.idNumber"/>
Then you can have another observable named valueBClass where you can set valueB's class as well as retrieve it from other observables.
valueBClass = ko.observable('Hello');
getClassFromValueB = function() {
var theClass = observables.valueBClass();
}
You're on the right track to keep the DOM and the model separate. This is an important concept that is sometimes missed when using Knockout. As a result of keeping them separate, you would not reference any DOM elements anywhere in your Knockout model. You would only reference observables.
Related
What is the best way to re-use composed html elements, in particular forms?
Say I have a basic voting form similar to this (in a separate file voteform.html):
<form>
<div>
<input id="bad" type="radio" name="vote" value="-1"/>
<label for="bad" style="background: #b66; width:100px; height:100px; display:inline-block"></label>
<input id="good" type="radio" name="vote" value="+1"/>
<label for="good" style="background: #6b6; width:100px; height:100px; display:inline-block"></label>
</div>
</form>
I now want to let users of my website vote on several questions using the same form. What is the best / cleanest / most elegant way to re-use the above code snippet throughout my website (the questions asked may change dynamically over time, but the answer options in the form would always be the same)?
If i simply .clone() the form or load the content via
var x = $('<div/>').load('./voteform.html');
votes_div.append(x)
the radio buttons do not behave as expected because the ids are the same for each clone and all changes will only affect the first form.
This question explains how to change the id when cloning elements, is this the best option i have? Seems like a bit of a workaround.
What is the best / cleanest / most elegant way to re-use the above code snippet throughout my website?
The simplest answer to that is look at how others did it before: templates, web components, libraries, frameworks. Here's an example with Handlebars.js, but you could just as well use any of the 1000's of others. Handlebars is a good start because it's minimal and has implementations in multiple programming languages, but it just comes down to separation of concerns (separating the data from the view) in the end.
var html = document.getElementById('form-template').innerHTML,
template = Handlebars.compile(html),
container = document.getElementById('questions');
var questions = [
{q: 'Ever squeezed a trigger?'},
{q: 'Ever helped a brother out when he was down on his luck?'},
{q: 'Got a little gouda?'},
{q: 'Hater?'},
{q: 'Wanna see a player get paper?'},
{q: 'You a boss player, you a mack?'},
];
container.innerHTML = template({
questions: questions
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.10/handlebars.min.js"></script>
<form id="questions"></form>
<script type="text/template" id="form-template">
{{#each questions}}
<label>{{ q }}</label>
<div>
<input id="yup{{#index}}" type="radio" name="vote{{#index}}" value="-1"/>
<label for="yup{{#index}}" style="background: #6b6; width:100px;">YUP</label>
<input id="nope{{#index}}" type="radio" name="vote{{#index}}" value="+1"/>
<label for="nope{{#index}}" style="background: #b66; width:100px;">NOPE</label>
</div>
{{/each}}
</script>
If you opt for nesting the template in an {{#each}} loop, use Handlebars' #index to add an index to every question id. You can also use strings instead of objects in the array, then you just have to change the {{q}} in the code above to {{this}}
Note: questions inspired by E-40 - Choices lyrics.
Here's how I might approach it from a purely JavaScript stand-point. I've used some ES6 here for convenience, but that can easily changed to ES5 if necessary.
1) Use a function that returns a template literal.
We can pass a question into the function a have it applied to the template very easily. I've added an extra class called inputs here which will be used to catch the click events from the radio buttons as they bubble up.
function newQuestion(question) {
return `<form>
<div>${question}</div>
<div class="inputs">
<input id="bad" type="radio" name="vote" value="-1" />
<label for="bad" class="square bad"></label>
<input id="good" type="radio" name="vote" value="+1" />
<label for="good" class="square good"></label>
</div>
</form>`
}
2) Set up a list of questions
const questions = [
'Do you like sprouts?',
'Do you like the color blue?',
'Do you like candles?',
'Do you like the ocean?'
];
3) Have some way to record the answers. Here's an empty array.
const answers = [];
4) (From the demo) pick up the id of the container where we're going to place the HTML from the template, and set the question index to 0.
const main = document.querySelector('#main');
let index = 0;
5) When we click on a good or bad id we want some way to handle that click. Here we check the id of the clicked element and then update the answers array depending on the id. We then advance to the next question.
function addAnswer(e) {
const id = e.target.id;
switch (id) {
case 'good':
answers.push(1);
break;
case 'bad':
answers.push(-1);
break;
}
showQuestion(++index);
}
6) The main function that checks to see if we've reached the end of the question array. If not it grabs new HTML by passing in the new question to the newQuestion function and adding it to main. Then we add a click event to inputs. If the questions are complete (in this example) it simply shows the answers array in the console.
function showQuestion(index) {
if (index < questions.length) {
main.innerHTML = newQuestion(questions[index]);
main.querySelector('.inputs').addEventListener('click', addAnswer, false);
}
console.log(answers);
}
7) Kick-starts the voting system.
showQuestion(index);
I don't know if this is the kind of approach you want to take, but I hope it helps in some way.
DEMO
This is some mark-up for a list of answers in a question editor.
var Answers = ko.observableArray(["Answer 1","Answer 2","Answer 3"]);
<!-- ko foreach: Answers -->
<div class="qa-box" data-bind="event: { mousedown: mouseDown, dragend: dragEnd, dragstart: dragStart }">
<div class="qa-body">
<div class="radio">
<label>
<input type="radio" data-bind="attr: { name: 'Q' + $parentContext.$index(), value: $index }, checked: $parent.CorrectAnswer" /><span></span>
Tick the correct answer
</label>
<a href="#" data-bind="click: function(d,e){ $parent.remove(d,e); }">
<i class="fa fa-times"></i>
Remove this answer
</a>
<div class="form-control" contenteditable="true" data-placeholder="Put answer text here" data-bind="ckeditor: $data, attr: { id: 'Q' + $parentContext.$index() + 'A' + $index() }"></div>
</div>
</div>
</div>
<!-- /ko -->
Answers is a ko.observableArray containing strings. The remove link is on the template for each string because that captures which string is to be removed, but the remove operation is defined on the object that owns the list.
Originally I coded it like this:
data-bind="click: $parent.remove"
and remove was called, but unsurprisingly this was the string rather than the parent object. So I rewrote it like so:
data-bind="click: function(){ $parent.remove($data); }"
which failed to change anything. Then I tried this
data-bind="click: function(d,e){ $parent.remove(d,e); }"
which works. Inside the remove method, this is the parent object, and the parameter is the string.
Given that the final form works, why doesn't function(){ $parent.remove($data); change this to the value of $parent inside the call to remove?
On a related note, I tried writing it like this
function(){ $parent.remove.apply($parent, $data); }
but this produced a JS error complaining that something wasn't a function. It didn't say what wasn't a function, and when I looked up apply on mdn I couldn't see anything wrong. What's up with that?
A more elegant solution to this precise problem is available from Anders's answer to How to access $parent or $parents[] in knockout viewmodel click event?
Ignore the accepted answer (same as my solution) and scroll down a little to learn about bind.
Attempted to log a label to console via
var labelTest = document.getElementById('js_8').label;
console.log(labelTest);
However it is returning undefined.
Edit: correcting some stuff, sorry at work and trying to do this in between other tasks. What my end result needs to be is targeting the inner html of the js_8 ID, but with React it is different for each of the Pages that it is on. So I want to add an extra stipulatoin of having that label attribute.
HTML:
<span data-reactroot="" label="1715724762040702" class="_xd6" data-pitloot-persistonclick="true" display="inline" data-hover="tooltip" data-tooltip-content="Copy Text to Clipboard" id="js_8"><div class="_xd7">1715724762040702</div></span>
I'm not sure exactly what you're after, but this is a way to connect a <label> and <input> together via JavaScript.
var some_id = 'someid',
my_label = getLabel(some_id);
function getLabel(id) {
return document.querySelector('[for=' + id + ']')
}
my_label.click();
<label for='someid'>My Label</label>
<input type='text' id='someid' />
You can associate a <label> with an <input>, <output>, <select> or <textarea> element in one of two ways:
The for attribute:
<label for="js_8">Test</label>
<input id="js_8">
Or by wrapping the element with a label:
<label>Test<input id="js_8"></label>
You can then access the associated label(s) as an array like this:
var labelsTest = document.getElementById('js_8').labels;
// labelsTest will be an array of 0 or more HTMLLabelElement objects
console.log(labelsTest);
Label-able elements can have more than one label.
So essentially I believe I am going to want to utilize var x = getAttribute("label") . The fact that the attribute was titled label confused me, and in turn I goof'd.
I am creating check-boxes in my ASP.NET code behind a file in C#. I am adding an attribute value before adding the control to the page, therefore ASP is adding the attribute to the span surrounding the check-box and the label for the text. It looks like this:
<span data-bind="visible: showCheckBox(this)">
<input id="" type="checkbox" name="ctl00$MainContent$SecondaryForm$ctl00" value="1">
<label for="">Outside Source</label>
</span>
I have a function called showCheckBox() written in Knockout.js. It determines if the target checkbox should be displayed, based on the value of the selected item in the drop down list immediately preceding it. For example, if the value of the selected item in the drop down list is 1, then the target checkbox with a corresponding value of 1 would be visible. That function looks like this:
function showCheckBox(span) {
var value = span.firstChild.value;
return value == reason();
}
reason() is the view model variable that holds the value of the selected drop down list item.
No matter what I do, I cannot get the value of the check-box to be sent correctly. It is always undefined.
The first child in this HTML is actually a textNode, which firstChild will return if that is what is found. These are the actual characters it is returning: "↵ " (A return and a space).
You can use the firstElementChild property instead:
function showCheckBox(span) {
var value = span.firstElementChild.value;
return value == reason();
}
Also, don't forget to check the support tables for firstElementChild.
After thinking about it for a while also, a coworked suggested this solution.
Instead of assigning the value of the check box as the ID that corresponds to the drop down list view model variable, he suggested I assign it in the data-bind attribute instead. In my C# code behind file, that looks like this
cb.Attributes.Add("data-bind", String.Format("visible: showCheckBox({0})", reasonDetails[i].ReasonForSendingNoticeID.ToString()));
Which looks like this when displayed in HTML
<span data-bind="visible: showCheckBox(1)" style="display: none;">
<input id="" type="checkbox"
name="ctl00$MainContent$SecondaryForm$ctl00" value="Outside Source">
<label for="" style="display: inline;">Outside Source</label>
</span>
Then I changed the function as follows
function showCheckBox(id) {
return id == reason();
}
Doing it like so allowed us to directly pass the value into the function without having to pass the element or its child.
Thank you for the help and suggestions.
The problem is hidden, and it's because you're for a large part not using KnockoutJS at all. You should check the docs for the KnockoutJS checked binding.
There should very likely be no reason to check the DOM inside view models, not even those handling visibility. Instead, use the view model. You haven't quite posted a complete minimal repro, but here's what it should more or less look like:
function ViewModel() {
var self = this;
self.reasons = [{id: 1, txt: "Main reason"}, {id: 2, txt: "Other reason"}];
self.reason = ko.observable(self.reasons[0]);
self.showCheckBox = ko.computed(function() {
return self.reason().id === 1;
});
self.hasOutsideSource = ko.observable(false);
};
ko.applyBindings(new ViewModel());
pre { font-family: consolas; background: smoke; border: 1px solid #ddd; padding: 5px; margin: 5px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<select data-bind="options: reasons, value: reason, optionsText: 'txt'"></select>
<span data-bind="visible: showCheckBox">
<label>
<input data-bind="checked: hasOutsideSource" type="checkbox" name="ctl00$MainContent$SecondaryForm$ctl00" value="1">
Outside Source
</label>
</span>
<hr>
This would be sent to your server / debug info:
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
If you can't take the above approach, I strongly recommend considering skipping the use of KnockoutJS, because it'll probably cause you more trouble than it'll help you if you do to much DOM manipulation on your own.
usually I can figure out a way to make Knockout-js do what I want. In this case however, i'm struggling a little, so I'm opening the question up to the community here on SO.
Introduction
I'm writing an HTML5 web app using typescript, bootstrap, knockoutjs and a nodejs backend.
In my UI which is all controlled via knockoutJS I have a set of buttons, formed as a bootstrap 3 button group of select-able options.
This justified group, gives me 4 buttons horizontally, but allows the behaviour of the button selections to remain consistant with a group of option buttons.
That consistancy is important, beacuse ONLY one button at a time can ever be selected, so when one is clicked, the rest deselect.
This is a default component in BS3, as the following image shows:
As you can see in the image, the 'Discarded' button is selected, to achieve this a class of 'active' must be added to the existing class list of the label element surrounding the inner radio element that makes up the control. The following HTML is used to create this image:
<div class="btn-group btn-group-justified" data-toggle="buttons">
<label class="btn btn-primary">
<input type="radio" name="options" id="option1" checked >Rejected
</label>
<label class="btn btn-primary active">
<input type="radio" name="options" id="option2">Discarded
</label>
<label class="btn btn-primary">
<input type="radio" name="options" id="option3">Held
</label>
<label class="btn btn-primary">
<input type="radio" name="options" id="option3">Header Added
</label>
</div>
All this works great except for one small flaw, I'm using knockout JS to manage the UI.
The Problem I'm Trying to Solve
The checked state of each of the options is tied to a property inside the view model applied to the HTML, so the inner option on the rejected button for example has a normal knockout-js checked binding added to it as follows:
<input type="radio" name="options" id="option1" checked data-bind="checked: reject">Rejected
Each of the options, each have their own backing field:
reject
discard
hold
addheader
and each of those backing fields are a standard boolean value holding true/false, what I can't figure out is how to add/remove the 'active' class on the enclosing label, to reflect which of these states has been selected.
To be more precise, I cant figure out the best way to do it elegantly.
Approaches I've tried
what I know works is to add a simple computed observable to each label that returns
"btn btn-primary active"
when that option is set to true, and
"btn btn-primary"
when it is not.
I know this, because in my view model, I had a simple function:
SenderDialogViewModel.prototype.isRejectSelected = function () {
if (this.reject == true) {
return "btn btn-primary active";
}
return "btn btn-primary";
};
However, this approach means 4 functions, one for each flag to test, making it difficult to add new flags at a later date.
What I'd like to be able to do, is something like the following:
<label class="btn btn-primary" data-bind="class: isSelected(reject)">
as an example.
Which I almost got to work with a slight modification to the above:
SenderDialogViewModel.prototype.isSelected = function (selectionVariable) {
if (selectionVariable == true) {
return "active";
}
return "";
};
Where selection variable could be any of the flags available in the view model, passed in.
The problem here was, that this ONLY updated the first time the UI was drawn, subsequent changes to the flags, failed to update the UI to reflect the given status.
To try and resolve this, I changed the function to a computed observable, only to then receive a JS error when the UI was drawn, stating that the computed observable had to have a 'write' handler added to it, because I was passing a parameter in.
If I need to add a write handler, then that's fine, but I'd rather not.
Summary
So in summary, there are ways of changing the class list in sync with other options, but most of them are messy, what I'm trying to do is create a way that's easily expanded as new buttons are added (This is important as some button sets are dynamically generated), rather than adding a handler to individually check and report the status on each and every variable there, in one function call that can be added simply into the view-model and re-used again and again.
Ok... and as it ALWAYS happens, no sooner do I post this, than I actually figure out how to make it work.
The solution was staring me in the face all along, in the form of the knockout js css binding.
To quote the knockout-js docs:
The css binding adds or removes one or more named CSS classes to the associated DOM element. This is useful, for example, to highlight some value in red if it becomes negative.
What this is saying, as "I can apply or remove a single class, to the collection of classes already present, based on the value of a variable in my view model"
So, the answer to my problem, quite simply becomes:
<div class="btn-group btn-group-justified" data-toggle="buttons">
<label class="btn btn-primary" data-bind="css: { active: reject }">
<input type="radio" name="options" id="option1" checked >Rejected
</label>
<label class="btn btn-primary" data-bind="css: { active: discard }">
<input type="radio" name="options" id="option2">Discarded
</label>
<label class="btn btn-primary" data-bind="css: { active: hold }">
<input type="radio" name="options" id="option3">Held
</label>
<label class="btn btn-primary" data-bind="css: { active: addheader }">
<input type="radio" name="options" id="option3">Header Added
</label>
</div>
Along with that, if I now just add the appropriate option/checked bindings to the option controls themselves, then everything should update correctly as needed when the buttons are clicked.
A little side note on working your own answer out
I think sometimes, the exercise of just having to think through your problem, while 'virtually' trying to describe it to others, triggers the brain to think in a different direction.
It certainly helped, that I was typing this when the penny dropped, but I proceeded and then answered/updated as appropriate, because it occurs to me that this is a question that will trip others up too, hopefully this will serve as an education to fellow travelers who might like me just be suffering from a sunday evening brainfart.
Update Monday 30-June 2014
It turns out, this was a little more tricky than I first anticipated. Sure I solved the main answer to my question about syncing the CSS for the button above. BUT... syncing the Option buttons also turned out to be quite a challenge, this update is to present a full end to end solution.
First, you need to mark up your HTML like this:
<p><strong>Rule type:</strong></p>
<div class="btn-group btn-group-justified" data-toggle="buttons">
<label class="btn btn-primary" data-bind="css: { active: reject }, click: function(){ changeType('reject'); }">
<input type="radio" name="ruletype" value="reject" data-bind="checked: selectedOptionString" >Rejected
</label>
<label class="btn btn-primary" data-bind="css: { active: discard }, click: function(){ changeType('discard'); }">
<input type="radio" name="ruletype" value="discard" value="true" data-bind="checked: selectedOptionString">Discarded
</label>
<label class="btn btn-primary" data-bind="css: { active: hold }, click: function(){ changeType('hold'); }">
<input type="radio" name="ruletype" value="hold" data-bind="checked: selectedOptionString">Held
</label>
<label class="btn btn-primary" data-bind="css: { active: addheader }, click: function(){ changeType('addheader'); }">
<input type="radio" name="ruletype" value="addheader" data-bind="checked: selectedOptionString">Header Added
</label>
</div>
The KEY take away's here are as follows:
1) The CSS rule must specify the class 'active' and be tied to your independent option flag that shows true/false for that option being selected.
2) You MUST have the click handler on the button, BS3 (as I found out) processes the click on the button NOT on the option control, due to how knockout works this HAS to be an inline function, passing in a single parameter, DO NOT be tempted to tie it directly to the computed observable used by the option, it won't work.
3) You must mark the option elements up as shown, that is they must ALL have the same name attribute, the value MUST match what you want that selected option to portray, and must match the strings your button handler is sending
4) Each option element cannot be bound to a simple variable, you need to pass it through a computed observable, not just because of how I'm handling them, but even for simple single Boolean switches it uses "true" & "false" as strings and not as Boolean's as you might expect it.
Once you've marked up your HTML, you then need to build a view model to support it all, in my case I actually did this using typescript then compiled to JS, the code I'm pasting in here is the JS code produced by the TS compiler.
first and foremost you need to make sure you have the following properties on your view model:
this.reject = ko.observable(false);
this.discard = ko.observable(false);
this.hold = ko.observable(false);
this.addheader = ko.observable(false);
(use self, this, me... or what ever it is you use to define your knockout models) the important thing is that they are simple ko observable boolean's
You also need a computed observable that has both a write and a read function:
this.selectedOptionString = ko.computed({
read: function () {
if (this.reject())
return "reject";
if (this.discard())
return "discard";
if (this.hold())
return "hold";
if (this.addheader())
return "addheader";
},
write: function (value) {
console.log("rejectstr:");
console.log(value);
if (value == "reject") {
this.reject(true);
this.discard(false);
this.hold(false);
this.addheader(false);
}
if (value == "discard") {
this.reject(false);
this.discard(true);
this.hold(false);
this.addheader(false);
}
if (value == "hold") {
this.reject(false);
this.discard(false);
this.hold(true);
this.addheader(false);
}
if (value == "addheader") {
this.reject(false);
this.discard(false);
this.hold(false);
this.addheader(true);
}
}
}, this);
This could probably be done a lot more elegantly, but essentially when an option in a group is activated under knockout, knockout takes whatever is in the 'Value' attribute and sends that as a string into your view model.
If you tied this to a simple observable, then that observable would get set to the string value of that attribute. For me however, because I have a series of 4 flags to set that control various states on the UI, a chained if then was appropriate (a switch or possibly a lookup array or hashtable would have worked just as well)
The ultimate outcome of the observable is that one boolean and one only ever be set at a time, and beacuse the CSS in the button is tied to this flag, then the active class gets applied to the given button for which ever is set to true.
For a read, you need to translate your flag state back to a string for knockout to compare to the values it knows about, so the read does the reverse.
For the button click handler, you have to do this inline as shown in the markup, this is beacuse knockout reserves some parameters for automatic things like element name, event info and other's, none of which we need here, so the simplest way is to inline it.
However, in lining it means your not tying to a property and so you can't tie it directly to the computed observable used by the option controls.
Instead what you need to do is add a small stub function to your view model as follows:
SenderDialogViewModel.prototype.changeType = function (newType) {
this.selectedOptionString(newType);
};
This does not need to be observable in any way as it only gets called one way when your button is clicked.
If you want to see the full solution, I'll be making the app this is part of available on my git-hub page free for people to use, but it's not finished yet.
Hope everything above however turns out to be useful for some folk, I have to admit, it turned out to be a little bit more of a challenge than I expected.
Shawty