So I have a the following textarea in my view that I am attempting to update via a knockout binding.
Here is the code from the View:
<textarea disabled id="CCHPISentence" style="width:99.7%; height:75px; resize: none; font-family:Verdana; overflow: auto;" data-bind="text: fullSymptomTextObservable"> </textarea>
Here is the Jquery function that applies the bindings, I am wondering if my issue is here:
$(function () {
ko.applyBindings(symptomTextViewModel, document.getElementById("CCHPISentence"))
})
Here is my ViewModel:
function symptomTextViewModel(fullText) {
if (fullText === undefined)
{ fullText = ""}
this.fullSymptomTextObservable = ko.observable(fullText.toString())
}
Here is a snip from the js function that calls my ViewModel. I am building the fullText variable in this 2nd js function:
//FINAL PARAGRAPH KNOCKOUT VM MAPPING
fullText = sentence1 + sentence2 + sentence3 + sentence4 + sentence5
var symptSentViewModel = new symptomTextViewModel(fullText)
symptomTextViewModel(fullText);
Thanks so much to anybody in advance who can assist me here. I feel like I am missing something stupid, I have tried this every different way that I can think of with no luck.
it would be easier to make fullSymptomTextObservable a pureComputed observable. That way as the various sentences change so will the full sentence. This way you are taking advantage of knockoutjs.
function SymptomTextViewModel(fullText) {
var self = this;
if (fullText === undefined) {
fullText = ""
}
self.fullSymptomTextObservable = ko.observable(fullText.toString())
}
var vm = new SymptomTextViewModel('This is a test paragraph. Watch for the alert();');
ko.applyBindings(vm,document.getElementById("CCHPISentence"));
alert("about to use the vm variable to access the text.");
vm.fullSymptomTextObservable('Changing it to something else');
alert("about to use the ko.dataFor function");
var testVm = ko.dataFor(document.getElementById("CCHPISentence"));
testVm.fullSymptomTextObservable("Maybe this would be better suited to what you need.");
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<textarea disabled id="CCHPISentence" style="width:99.7%; height:75px; resize: none; font-family:Verdana; overflow: auto;" data-bind="text: fullSymptomTextObservable"></textarea>
First of all I would fix all missing semicolons in your code. Than check if scope of 'this' in your view models function is correct. Some info from browser console would be also helpful. Check if there isn't any errors which knockout would throw.
Related
I know I'm overlooking something but I'm stuck on knowing what I'm doing wrong. Can't seem to get the console to print out ( I'm eventually working on adding a box to the screen). I'm using Chrome btw:
HTML
<button id="1" class="hot"></button>
JS
function addBox() {
console.log("hello");
}
var clickBox = document.getElementById("1");
clickBox.onClick = addBox;
DOM properties are case sensitive (unlike HTML attributes) and the correct name of the property is onclick:
clickBox.onclick = addBox;
Learn more about the different ways of binding event handlers.
function addBox() {
console.log("hello");
}
var clickBox = document.getElementById("1");
clickBox.onclick = addBox;
.hot {
width: 100px;
height: 100px;
border: 3px solid black;
background-color: pink;
}
<button id="1" class="hot"></button>
Try
clickBox.onclick = addBox;
or
clickBox.addEventListener('click', addBox);
I do not know of any native onClick method for DOM elements in JavaScript.
You could do an event attribute in your HTML <button onclick="addBox()">.
Or you could do clickBox.addEventListener('click', addBox);.
this this code javascript :
var clickBox = document.getElementById("1");
clickBox.onclick=addBox;
function addBox() {
console.log("hello");
}
First, your ID should begin with a letter (if you plan to have HTML4 compatibility). Second, if you want to define an event using JS (without a library such as jQuery), you should use addEventListener. Or you can simply go the route of adding onclick directly to the element (onclick="addBox()"). Finally your onclick should all be lowercase (as JS property keys are case sensitive).
Try giving all the javascript within window.onload tags
Like the following:
window.onload = function(){
//Your Code Here
}
I have an website which uses an input type=file to upload some files.
After uploading I don't want to display the file selection, so I am going to delete it by calling event.target.value = null.
function viewModel() {
var self = this;
self.addAudioFile = function(data, event) {
event.preventDefault();
var context = ko.contextFor(event.target);
var selectedFile = event.target.files[0];
var targetPrompt = data.prompt;
console.log("aa");
event.target.value = null;
}
}
$(document).ready(function() {
var newModel = new viewModel();
ko.applyBindings(newModel);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class="btn-link prompt-audio-control">
<div class="btn-file">
<input type="file" data-bind="event: {change: $root.addAudioFile}" accept="audio/wav|audio/pcm|audio|vox">
<span> Upload </span>
</div>
</div>
What confuses me here is when I used Chrome to test, the function addAudioFile is executed only one time, but when I used IE, it executed this function 2 times.
I used the console.log("aa") in oder to test it. Can anyone explain me why this happens?
It was triggered twice due to this call.
event.target.value = null;
After that line, the change event is fired again.
Try putting another console.log after that line so you can see the effect.
Something like:
event.target.value = null;
console.log("bb");
So in order to stop the function from continuing its second call you can trap the value of event.target.value.
self.addAudioFile = function(data, event) {
if(event.target.value === ""){
return;
}
... rest of code here ...
}
Why is it checked as a string and not a null? That part i'm not sure but the browsers engine are converting it into a blank string hence the condition.
And on the part where chrome doesn't fire twice, i'll just leave this to browser experts, but my guess is chrome engine is set to not change the file if a null value is given (just my wild guess though).
==================
And lastly, you can use knockout this way but you "should" not do it that way :D
See Tomalak's answer on how to separate knockout's DOM concerns using custom bindings.
As a matter of principle, whenever you find that you access DOM elements, events or even the binding context in a knockout viewmodel then you are doing something wrong.
The only place in a knockout application where you interact with the DOM is the binding handler. If there is no binding handler that does what you need, write one.
// exemplary binding handler ---------------------------------------------------------
ko.bindingHandlers.file = {
init: function (element, valueAccessor) {
if ( !ko.isWriteableObservable(valueAccessor()) ) return;
// alternatively $(element).on("change", ...
ko.utils.registerEventHandler(element, "change", function (e) {
var observable = valueAccessor();
observable(e.target.files[0]);
element.value = null;
e.preventDefault();
});
}
};
// usage -----------------------------------------------------------------------------
function FileUploader() {
var self = this;
self.audioFile = ko.observable();
}
$(function() {
var viewModel = new FileUploader();
ko.applyBindings(viewModel);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class="btn-link prompt-audio-control">
<div class="btn-file">
<input type="file" data-bind="file: audioFile" accept="audio/wav|audio/pcm|audio|vox">
<span> Upload </span>
</div>
</div>
<hr>
View Model:
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
If setting the <input type="file">'s value to null is a good idea is another matter, since the event's files collection contains information about the files the user selected, it does not contain the files themselves. Setting the form control to null effectively destroys the user selection. If you want to actually upload the file at some point, more plumbing will be necessary.
What i undestand:
You want to "hide" the text about "what file you upload" after upload the file... (what redundant phrase lol), maybe this could help you instead "erasing" the selection.
Creating a HTML Button:
.fileUpload {
position: relative;
overflow: hidden;
margin: 10px;
}
.fileUpload input.upload {
position: absolute;
top: 0;
left: 0;
margin: 0;
padding: 0;
font-size: 20px;
cursor: pointer;
opacity: 0;
filter: alpha(opacity=0);
}
<div class="fileUpload btn btn-primary">
<span>Upload</span>
<input type="file" class="upload" />
</div>
I need to subscribe to an existing binding of a DOM element. As an example, I have the following input element:
<div id="MyDiv">
<input id="MyInput" data-bind="enable: isEnabled()" />
</div>
Now, assuming I only have access to the DOM element, I need to do something like this:
var inputElement = $("#MyInput");
var bindings = ko.utils.getBindings(inputElement); // getBindings does not exist
var enableBinding = bindings["enable"];
if (enableBinding != undefined) {
enableBinding.subscribe(function (value) {
if (value == false)
$("#MyDiv").addClass("disabled");
else
$("#MyDiv").removeClass("disabled");
})
}
Is there a way to do this?
Update: I've extended the sample so that you see my use case for this: The div here is automatically generated by a preprocessor and needs the disabled class on it when the input is disabled. It does not work if the attribute is only changed on the input element. The addition/removal must be transparent...
Short answer: Don't do this. There is a reason that getBindings is not a particularly visible function in the Knockout toolkit.
Long answer: You can, through a bit of indirection, get at the original binding.
HTML:
<div id="MyDiv">
<input id="MyInput" data-bind="enable: isEnabled" />
</div>
<input type="checkbox" data-bind="checked: isEnabled" />
JS:
var viewModel = function() {
self.isEnabled = ko.observable(true);
}
ko.applyBindings(new viewModel());
var input = $('#MyInput')[0];
function getBinding(element, name) {
var bindings = ko.bindingProvider.instance.getBindings(input, ko.contextFor(input));
return bindings.hasOwnProperty(name) ? bindings[name] : null;
}
var binding = getBinding(input, 'enable');
binding.subscribe(function(value) {
if (value == false)
$("#MyDiv").addClass("disabled");
else
$("#MyDiv").removeClass("disabled");
});
Working JSFiddle
EDIT: Found a shorter way
Again, if there is any way you can convince your preprocessor to add a CSS observable, do so. Mucking about with bindings in this manner relies on the particular quirks of Knockout 3.3.0's internal implementation, which can change in future releases.
Checkout the answer provided here.
In short, you can use
var viewModel = ko.dataFor(domElement);
to get the viewmodel that is bound to that DOM element. You can then, subscribe to any observables attached to that viewmodel.
Before I knew about things like Angular and jQuery, there was plain old Javascript like this:
function toggleClass(e, c) {
var classes = e.className.split(' ');
var match = false;
for(var i=0; i<classes.length; i++) {
if(classes[i] === c) {
match = true;
classes.splice(i,1);
break;
}
}
if(!match) classes.push(c);
e.className = classes.join(' ');
}
I've used this in the past to toggleClass name in an onclick event like so:
<div onclick="toggleClass(this,'foo')"></div>
Here is a working JSFiddle.
How would I implement this as a directive in Angular?
You can use AngularJs' ng-class directive instead of creating another directive.
angular.module('demo', [])
.controller('Ctrl', function($scope) {
$scope.toggleRed = true;
});
.box {
padding: 50px;
display: inline-block;
background-color: #efefef;
}
.box.red-box {
background-color: red;
}
<div ng-app="demo" ng-controller="Ctrl">
<div class="box"
ng-class="{'red-box': toggleRed}"
ng-click="toggleRed = !toggleRed">Click Me</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
Angular is not jQuery, so your thought process should not be about adding removing classes or showing hiding elements or anything to do on these lines.
Please refer to this SO post for some good pointers "Thinking in AngularJS" if I have a jQuery background?
In Angular model drives the view.
What you are doing should be done using the standard ng-class directive.
Let's say you have a grid of users and you want highlight rows when the user click on the row signifying the user has been selected. The way you would go about it would be to define the row html as
<tr ng-repeat='user in users' ng-click='user.selected=!user.selected' ng-class={'active': user.selected}>
</tr>
Now the state of user.selected drives the view and toggles the class on every click.
Is it possible to access bound element from ko.computed function?
Something like this pseudo code (simplified for clarity):
<h1 id="id1" data-bind="visible: myComputed">
<h1 id="id2" data-bind="visible: myComputed">
<h1 id="id3" data-bind="visible: myComputed">
...
self.myComputed = ko.computed(function(){
return <BOUND_ELEMNT>.id == 'id2';
});
Resulting in only the second element showing.
Note: I'm aware I can have a separate computed for every element, but it is not possible in my case.
EDIT:
Ok - I'll give a more accurate example. The following is similar to what I have:
<section id="id1" data-bind="visible: myComputed1">A lot of code</section>
<section id="id2" data-bind="visible: myComputed2">different lots of code</section>
<section id="id3" data-bind="visible: myComputed3">Other lots of code</section>
...
// This field's value changes over time (between 'id1', 'id2' and 'id3').
// Some complex logic changes this field,
// and as a result only one h1 is showing at a time.
self.myField = ko.observable();
self.myComputed1 = ko.computed(function(){
return self.myField() == 'id1';
});
self.myComputed2 = ko.computed(function(){
return self.myField() == 'id2';
});
self.myComputed3 = ko.computed(function(){
return self.myField() == 'id3';
});
This is an ugly violation of the DRY principle, and I would like to find a way to refactor it. The pseudo code above may solve it, but I'm open for suggestions...
You've created a short, simplified example, which is normally great. However, it feels like you've introduced an XY-problem instead. As such, this answer may or may not be helpful.
You're trying to introduce a dependency on the View in your ViewModel. It should be the other way around! Something like this would make more sense:
<h1 data-bind="visible: myComputed, attr { id: myId }"></h1>
Note the use of the attr binding to set the id. Your ViewModel should be constructed accordingly:
var activeHeaderId = 'id2';
var viewModel = function(id) {
var self = this;
self.myId = ko.observable(id);
self.myComputed = ko.computed(function() {
return self.myId() === activeHeaderId;
});
}
Note: I'm leaving my other answer as an answer to the first bit of the question, maybe it'll help other users stumbling upon this question.
The question in the update is:
This is an ugly violation of the DRY principle, and I would like to find a way to refactor it.
OP indicates in comments that answers focussing on the given example are preferred. The sample code can be easily refactored such that it doesn't violate DRY anymore. (PS. I still think the "why" behind wanting this structure is very important, but that doesn't prevent me from answering the question in the context of the given sample.)
Method 1 - One h1 in the View - One item in the ViewModel
Use the following View:
<h1 data-bind="attr: {id: myField}">
With this ViewModel:
self.myField = ko.observable();
Very DRY, functionally almost equivalent (except that the other 2 h1 elements aren't just invisible, they're not even in the DOM).
Method 2 - Multiple h1s in the View - Multiple items in the ViewModel
Refactor the View to a list structure (see note 4 in the foreach binding):
<!-- ko foreach: items -->
<h1 data-bind="attr: {id: myId},
text: itemName,
visible: $root.currentItem().myId() === myId()">
</h1>
<!-- /ko -->
With the following ViewModel:
var item = function(nr) {
this.itemName = ko.observable("Item number " + nr);
this.myId = ko.observable("id" + nr);
}
var viewModel = function() {
this.items = ko.observableArray([new item(1), new item(2), new item(3)]);
this.currentItem = ko.observable();
}
See this fiddle for a demo.
Method 3 - One h1 in the View - Multiple items in the ViewModel
With this method you use the list like setup from method 2, but only render the currentitem. The View utilizes the with binding:
<!-- ko with: currentItem -->
<h1 data-bind="attr: {id: myId}, text: itemName"></h1>
<!-- /ko -->
The ViewModel is the same as in method 2. See this fiddle for a demo.
Make a custom binding handler that uses your observable to trigger the visibility. Something like this:
ko.bindingHandlers.idVisible = {
update: function(element, valueAccessor) {
var idUnwrapped = ko.utils.unwrapObservable(valueAccessor());
if(idUnwrapped == $(element).attr('id'))
{
$(element).show();
}
else
{
$(element).hide();
}
}
};
Change your HTML:
<h1 id="id1" data-bind="idVisible: headerId">Header 1</h1>
<h1 id="id2" data-bind="idVisible: headerId">Header 2</h1>
<h1 id="id3" data-bind="idVisible: headerId">Header 3</h1>
And a sample view model:
function ViewModel() {
var self = this;
self.headerId = ko.observable('id1');
}
var vm = new ViewModel();
ko.applyBindings(vm);
Here's a jsFiddle with a demo that changes the headerId after two seconds: http://jsfiddle.net/psteele/cq9GU/