I'm new to using Knockout and am doing a very basic implementation that changes the color with a observable. Is there a cleaner way to write the following code?
<div class="selected" data-bind="style: { background: fullHexCode(mainScreenNavigationSelector()) !== false ? fullHexCode(mainScreenNavigationSelector()) : 'white' }"></div>
I have this in multiple spots on my page and they all use different params for the fullHexCode() function. It looks extremely messy. Any help would be great, thanks!
It looks like the logic depends on another observable so you could use a computed observable -- in the snippet below the backgroundColor computed observable depends on the mainScreenNavigationSelector observable.
That's just a simple example, you'll have to adjust it to your specific situation.
var MyViewModel = function () {
this.mainScreenNavigationSelector = ko.observable(false);
this.backgroundColor = ko.computed(function() {
return this.mainScreenNavigationSelector() ? 'green' : 'red';
}, this);
this.toggleColor = function() {
this.mainScreenNavigationSelector(!this.mainScreenNavigationSelector());
}
}
var viewModel = new MyViewModel();
ko.applyBindings(viewModel);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class="selected" data-bind="style: { 'background-color': backgroundColor }">
TEST
</div>
<button data-bind="click: toggleColor">Toggle Color</button>
You may deduplicate your HTML code by defining methods in your viewmodel. Named computeds are even better as they are naturally memoized, that is evaluated only once if used repeatedly in your HTML.
You may also factorize heavy expressions as with: or let: bindings in the parent node. For example: <div class='some-container' data-bind="let: { v: mainScreenNavigationSelector() }">... bindings based on v here... </div>.
Notice: let is better than with for this purpose. But it's a new binding which will be available in the next release of KO. You can polyfill it with a custom binding.
When JS expressions can't be avoided in your HTML code, try to make them as slick as possible. For example:
<div data-bind="style: {
background: fullHexCode(mainScreenNavigationSelector()) || 'white'
}"></div>
In Javascript, logical operator doesn't return true or false but the actual value of the last evaluated arguments. So:
a || b return a if not "falsy", otherwise b
a && b return b if a not "falsy", otherwise a
The last idiom is useful in KO bindings because contrary to Angular, KO bindings are regular JS expressions. They fail if some null/undefined occurs in a dot sequence (like a.b if a is undefined).
So instead of some tertiary operator hell like data-bind="text: object != null ? (object.a != undefined ? object.a : 'None') : 'None'", just write data-bind="text: object && object.a || 'None'"
Also [] and {} are not falsy, and it's actually a good thing. It allows to write things like data-bind="foreach: foo.optionalArray || ['default', 'stuff']"
However, Number(0) is false. So beware of something like data-bind="with: object.id && 'saved' || 'new'" which may not work as expected if 0 is a valid object id.
Also this last trick. If data-bind="text: name" fails because name is undefined , then "data-bind="text: name || 'anonymous'" will still fail, but "data-bind='text: $data.name || 'anonymous'" will work as expected. As a convention, I write $data.attribute instead of attribute to convey the info about dealing with a optional attribute.
Related
I am working with a child component where an array item is passed as prop. However, that item also contains another array where there should be null and undefined check before rendering. Adding || [] to each filed didn't seem the best way. Is there a better way that I am missing?
const ChildComponent= ({ newsItem, textColor, newsItemHeadingColor }) => {
const imageFormats = newsItem?.itemImage?.formats;
const imageSrc = useProgressiveImage({ formats: imageFormats });
const history = useHistory();
const { state } = useContextState();
const { schemeMode, colorSchemes } = state;
const { itemTagColor, itemTagTextColor } = getThemeColors({
schemeMode,
colorSchemes,
colors: {
itemTagColor: newsItem?.itemTagColor?.name,
itemTagTextColor: newsItem?.itemTagTextColor?.name,
},
});
return (
<NewsItem
onClick={() => {
const url =
newsItem?.itemTag === 'Lab'
? getPageLink({
page: newsItem?.itemLink || [], <<<<<< this check
})
: newsItem?.itemLink || []; <<<<<< this check
url && history.push(url);
}}
>
<ImageContainer>
<Image src={imageSrc} alt={`${newsItem?.itemImage || []}`} /> <<<<<< this check
</ImageContainer>
<NewsItemTag color={itemTagColor} itemTagTextColor={itemTagTextColor}>
<Body4>{`${newsItem?.itemTag || []}`}</Body4> <<<<<< this check
</NewsItemTag>
<NewsItemName newsItemHeadingColor={newsItemHeadingColor}>{`${
newsItem?.itemTitle || [] <<<<<< this check
}`}</NewsItemName>
<Description textColor={textColor}>{`${
(newsItem.description &&
newsItem?.itemDescription) || [] <<<<<< this check
}`}</Description>
</NewsItem>
);
};
In fact yes, you have many many ways for improving your code and the readabilty of it.
In your <NewItem /> onClick function:
I have nothing to say for the first one, it seems correct since you want the item link or an empty array
For the second one, your are already using the optional chaining operator, the "?". This operator check if there is a value, if not, it returns null. That means that should be enough, since you check after your url variable is defined or not. So simply write that : : newsItem?.itemLink
For the third one, the one on your <Image /> component, First there is no need at all to put everything in backquote. Simply alt={newsItem?.itemImage || []} should work fine. Then, alt attribute is taking a string, so maybe change it with alt={newsItem?.itemImage || ""}
For the three last one, they are basically the same, you have content to display and you want to display nothing if there are empty. The fact here is that your are rendering some elements even if there is no content and this is not an optimized way. The cleanest way in my opinion in order to make your template more readable will be to define variable before your return function and based on this variable, you will display the appropriate section :
Before your return function:
const hasTag = !!newsItem?.itemTag;
const hasTitle = !!newsItem?.itemTitle;
const hasDescription = !!newsItem.description && !!newsItem?.itemDescription;
In your template:
{hasTag && <NewsItemTag color={itemTagColor} itemTagTextColor={itemTagTextColor}>
<Body4>{newsItem.itemTag}</Body4>
</NewsItemTag>}
{hasTitle && <NewsItemName newsItemHeadingColor={newsItemHeadingColor}>
{newsItem.itemTitle}
</NewsItemName>
{hasDescription && <Description textColor={textColor}>
{newsItem.itemDescription}
</Description>
I hope this was clear, hit me up if this wasn't.
Happy coding :)
There are multiple ways that you could handle this in javascript, assume that we have a component that receives a prop called newsItem which is an object
You could do that in the following ways
Using the logical operator or (es6)
const itemLinks = newsItem.links || []
you dont need the optional chaining operator (?.) here if you know that newsItem is an object and not undefined.
This is the shortest syntax, and it works fine as long as links is either undefined or an array
however it becomes problematic if later links become a boolean variable, which will yield incorrect results
This method is not safe, and it becomes cumbersome to handle as properties become nested deeply
Using lodash get method or some other library or a custom solution (es6)
because in es6 the only practical way was to either the logical operator and this was a common problem, custom solutions were brought it, basically any solution should have two advantages over the logical or operator
They should work fine with boolean values
They should be easier to work with deeply nested values
custom someDeepleyNested = get(newsItem, 'property1.property2.property3', 'defaultValue')
// there is no need for property1, property2, property3 to exist at all
Optional chaining operator and Nullish coalescing Operator (stage 4 finished proposals)
Because this was a common problem for all javascript developers,the language had to bring a native solution
const someNestedProperty = newsItem?.property1?.property2?.property3 ?? 'someDefaultValue'
// notice that both the Optional chaining operator and Nullish coalescing Operator, work with both undefined and null
Specific to react: use default props
const Component = ({newsLink}) => {}
Component.defaultProps = {
newsItem: {
itemLinks: []
}
}
(Using KnockoutJs 2.0.0)
I have a list of phone numbers in my viewmodel. Each phone number has a type (home, work, mobile, etc). I want to display an icon (based on a fontawesome class) next to each phone number.
If I hardcode the icons in the css binding, everything works:
<tbody data-binding="foreach: phoneList">
<tr>
<td><span data-bind="css: {'icon-home' : TypeId() == 1, 'icon-building': TypeId() == 2, ... , 'icon-phone': TypeId() >= 7></span></td>
...
</tbody>
I wanted to replace the hardcoded list with a call to a function. I initially tried adding the function to the parent but had no success, so then I tried adding the function directly to the phone object itself both as a function and as a ko.computed() -- but neither of these work for me.
I've dummied up some code here that demonstrates the problem. If you inspect the span element of the table items, you'll see that it almost appears as if the data-biding is treating the returned string as an array of characters and setting the class based on indexes rather than treating the returned string as a class.
I'm sure this is something completely obvious, but I've been beating my head to no avail.
A computed observable should work just fine. The problem is what what you're returning from that computed observable. You need to return the definition of classes in the same format as the hard-coded version:
me.getClass = ko.computed(function() {
return me.typeId() == 1 ? { 'mobile': true } : { 'business': true };
});
See the updated version here: http://plnkr.co/edit/qDjgMlZpXHjn5ixY3OCt
Or, you could define a custom binding to clean up the computed function a bit, though it should be noted that in this case all classes will be replaced by the output of the binding. This is probably not necessary in Knockout 3.0.0, as alluded to in the comments and other answers.
Binding:
ko.bindingHandlers.setClass = {
update: function(element, valueAccessor, allBindings) {
var value = ko.utils.unwrapObservable(valueAccessor());
element.className = value;
}
};
Observable:
me.setClass = ko.computed(function() {
return me.typeId() == 1 ? "mobile" : "business";
});
HTML:
<td data-bind="setClass: setClass, text: typeId"></td>
A version using a custom binding is here: http://plnkr.co/edit/ryaA4mIf7oh5Biu8bKj0?p=info
Fix
Example
I updated your version of KO to 3.0.
Next, I changed your ko.computed binding for getClass from:
me.getClass = ko.computed(function() { return me.typeId == 1 ? "mobile" : "business"; });
to:
me.getClass = ko.computed(function() { return this.typeId() == 1 ? "mobile" : "business"; }, me);
Note
There may be a way to do this with KO 2.0, but I couldn't find documentation for previous versions. I imagine the issue is related to syntax if the feature exists.
An alternate way to do this is use an attr data-bind, instead of using a custom binding handler to set the class on the element.
So, you would still need to use a computed to set the observable:
me.setClass = ko.computed(function() {
return me.typeId() === 1 ? "mobile" : "business";
});
Then use an attr binding to set the class on the html element:
<td data-bind="attr: { class: setClass }, text: typeId"></td>
So, i'm simply trying to convert an int to a bool inside of an "if" statement. I'm unsure on how to achieve this. My code that I was playing with is below.
JsFiddle code here
The HTML:
<div id="main-div"></div>
<script id="testTmpl" type="text/x-jsrender">
<input data-link="{:test:}" />
<div data-link="{intToBool:test:}"></div>
// testing different methods, none seem to work
{^{if intToBool:test }}
Woot! 1
{{/if}}
{^{if intToBool(test) }}
Woot! 2
{{/if}}
{^{if ~root.intToBool(test) }}
Woot! 3
{{/if}}
</script>
The JavaScript:
$.views.converters({
intToBool: function(val) {
if (val === 0 || val === false || val === '0' || val === 'false') {
return false;
} else {
return true;
}
}
});
function App(data) {
self = this;
self.test = 0;
};
var app = new App();
var testTmpl = $.templates("#testTmpl");
testTmpl.link("#main-div", app);
Registering converters is only for use with the {{: ...}} tag (http://www.jsviews.com/#assigntag) in it's various forms {{cvt:expression}} {^{cvt:expression}} <div data-link="{cvt:expression}"> or <input data-link="{cvt:expression:cvtBack}" />. See http://www.jsviews.com/#converters.
For other scenarios you need to use either a method {^{if someMethodOnMyModel(test)}} or a helper (http://www.jsviews.com/#helpers): {^{if ~someHelper(test)}}.
See also http://www.jsviews.com/#samples/jsr/paths for examples of helper paths - ~some.expression... - to either helper function or helper objects. Note that ~root is a built-in helper path to the top-level data object that you passed in to render() or link().
Edit: In fact there is a little-known feature in JsRender and JsViews which does let you associate registered converters with other tags. You can write
{^{if test convert="intToBool" }}
Woot!
{{/if}}
But generally it may be simpler/better to use ~intToBool(test) - which is more familiar, and perhaps easier to understand when reading the template.
Also BTW {{if test}} tests on 'truthy' so test does not need to be of type boolean. OTOH if you want to make "false", or "0" be falsey, then you will indeed need a helper/converter.
I have a situation where I need to replace a certain item in an observable array at a certain position of it. Right now I am doing it below with the slice method. Is there a better way that is built in to knockout.js to do this at a certain position? I was even thinking about doing a push, and then do a sort on that row with a order property but I have lots of rows and thought that was to much.
var position = ko.utils.arrayIndexOf(self.list(), game);
if (position != -1) {
self.list.remove(self.game);
self.list.splice(position, 0, newGame);
}
Code With Replace, Trying To Update Property Matchup That Has A New Property Called Name
var game = self.game;
if (game) {
var position = ko.utils.arrayIndexOf(self.list(), game);
if (position != -1) {
if (game.Matchup) {
game.Matchup = new Matchup(response.Data);
game.Modified(true);
}
else if (self.game) {
game = new Matchup(response.Data);
}
self.list.replace(self.list()[position], game);
}
}
HTML
<!-- ko foreach: Games -->
<td class="item-container draggable-item-container clearfix">
<div class="item clearfix draggable-active draggable-item" data-bind="draggableCss: { disabled: $data.Disabled(), matchup: $data.Matchup }, draggableGameHandler : { disabled: !$data.Matchup, disabledDrop: $data.Disabled() }, delegatedClick: $root.members.eventSchedule.editGame.open.bind($data, true, ($data.Matchup && $data.Matchup.Type == '#((int)ScheduleType.Pool)'), $parent.Games)">
<span data-bind="if: $data.Matchup">
<span data-bind="attr: { title: Matchup.Title }"><span data-bind="html: Matchup.Name"></span></span>
</span>
</div>
</td>
<!-- /ko -->
data-bind="html: Matchup.Name" doesn't update with replace.
Replacing an item in an observable array
The replace method is one option for replacing an item in an observable array. In your case, you could call it like this:
list.replace(game, newGame);
Bindings update when an observable dependency changes
But your question isn't only about replacing an item in an array. You've stated that the binding html: Matchup.Name isn't updated, so let's look at what could cause it to update:
If Name is an observable, modifying Name will cause an update.
If Matchup is an observable, modifying it will cause an update, but then you'd have to bind it like Matchup().Name and update it like game.Matchup(Matchup(response.Data));.
Replacing the entry in the observable array (is it Games or list?) with a new object will cause the whole inner template to re-render, obviously replacing each binding.
Looking through your code, I can see that in one case (if (game.Matchup)), none of these three things happen, and thus there's no way Knockout can know to update the binding. The first two obviously aren't occurring and although you do call replace on the array, it's the equivalent of this:
list.replace(game, game); // replace the item with itself
The foreach binding doesn't see the above as a change and doesn't update anything. So, to update the binding, you need to make a real update to an observable.
Further comments
To get the index of an item in an observable array, use the indexOf method:
var position = self.list.indexOf(game);
To replace an item at a specific index, use the splice method with a second parameter of 1:
self.list.splice(position, 1 /*how many to remove*/, newGame);
Observable arrays have a built in .replace method you can use to do this:
var position = ko.utils.arrayIndexOf(self.list(), game);
self.list.replace(self.list()[position], game);
If I have an expression {{ x }} and x is undefined or null, then how can I display a placeholder for it?
I provided one solution in my answer, but I would like to know what other ways there are.
Maybe, also for placeholder for promises.
{{ counter || '?'}}.
Just pure javascript. || can be used as default value. Since it would be different empty messages in each, a generalized directive would not be suitable for many cases.
If you want to apply a different class to empty ones, that's also built-in:
<div ng-class="{empty: !counter}" ng-bind="counter || ?"></div>
I would do it like this, but maybe there is a better way:
angular.module('app').filter('placeholdEmpty', function(){
return function(input){
if(!(input == undefined || input == null)){
return input;
} else {
return "placeholder";
}
}
});
and then use {{ x | placeholdEmpty}}
I do it with ng-show, like this:
<strong>{{username}}</strong>
<span class="empty" ng-show="!username">N/A</span>
Sure, it adds a lot more elements to my view that I might be able to handle differently. I like though how easy it is to clearly see where my placeholder/empty values are, and I can style them differently as well.
Implement default filter:
app.filter('default', function(){
return function(value, def) {
return (value === undefined || value === null? def : value);
};
});
And use it as:
{{ x | default: '?' }}
The advantage of the filter solution over {{ x || '?'}} is that you can distinguish between undefined, null or 0.
Implementing default-ish filters works, but if you're using only numbers you can use angular's own number filter
If the input is null or undefined, it will just be returned. If the
input is infinite (Infinity or -Infinity), the Infinity symbol '∞' or
'-∞' is returned, respectively. If the input is not a number an empty
string is returned.
{{ (val | number ) || "Number not provided"}}