How can I do string interpolation in a string variable in React? - reactjs

I have a variable coming from a static JSON file:
`const label = json.myString`
Where json.myString is "Hello, ${ name }". Since this is a variable, I do not know what it would be before hand.
And I want to do some string interpolation in my react component:
<div>{ label }</div>
How can I do this? Bonus points for a backup/default option if xyz is undefined

You should use template engine for this, for example, Mustache.
However, you can use an eval, but you don't want to use it, because it is unsafe in your case (some injections can be performed, if someone will perform a MITM-attack for your server, for example).
var a = "kappa"
console.log(eval("`Hello, ${a}!`"))
The other option that you can use is regular expressions.
There are 2 moments, that you should notice in this case:
Variables must be exist in some context, e.g. this, to allow stringified variables names be passed as key
You should not use special symbols of your template bounds inside template, or you should additionaly handle each special symbol inside regular expression.
var template = "My awesome template says: \"${ say }\""
var data = { say: "Hello!" }
console.log(template.replace(/\$\{([^}]+)\}/g, (match, group) => {
return data[group.trim()]
}))
By the way, this question is not React-specific, I suppose, but JS-specific. React doen't provide any profit features for this kind of logic tasks :)

The purpose of template literals is to avoid the usage of "some string" + "another string", and not much functionally.
In this scenario, it would be best to decouple the name with Hello.
That way, you could call
<div>Hello, ${name} </div>
In case you'd like to create a flexible greeting as well, you could then add the greeting as another string. E.g.:
{
name: "John Doe",
greeting: "Hello"
}
And go,
<div>${greeting}, ${name}</div>

Related

Capitalising interpolated language keys

The problem:
With i18next you can interpolate variables into your string like "AddNew": "Add new {{item}}" Now I have a language where the grammar requires the {{item}} needs to be the first word like "AddNew": "{{item}} toevoegen".
In the docs they show some support for formatting, but these are all related to dates or numbers. I am looking for a way to add a capitalisation formatting function e.g: "AddNew": "{{item, capitalise }} toevoegen"
What I have so far but isn't working
A function that capitalises the first letter.
export function capitalizeFirstLetter(string: string): string {
return string.charAt(0).toUpperCase() + string.slice(1);
};
And attempting to add it to the formatters
i18n.services.formatter?.add('capitalize', (value, lng, options) => {
return capitalizeFirstLetter(value);
});
Now I had to add a ? after .formatter for TS to not complain. But I'd guess that this is also the issue, formatter is undefined. I can only import Formatter and Services with a capital letter from the i18next library, so I'm a bit lost on how to continue.
Make sure you add your custom formatter AFTER you called i18next.init(): https://www.i18next.com/translation-function/formatting#adding-custom-format-function

How to pass a lambda expression to an AngularJS directive

I'm trying to create a set of AngularJS directives that will process an array of objects and perform specific operations using either the objects themselves or perhaps a property or sub-property of the each instance.
For example, if the array contains strings, one such directive might render a comma-separated list of those strings. I anticipate using such a directive like this:
<csv-list items="myArray" />
However, as stated above, I want the implementation to be flexible enough to pass an array of objects to the directive, whereby the directive can be instructed to act on a specific property or sub-property of each instance. If I could pass a lambda expression to the directive, I would imagine using it something like this:
<csv-list items="myArray" member="element => element.name" />
I guess there's a recommended AngularJS pattern to solve such problems, but I am quite new to AngularJS, so I haven't found it yet. Any suggestions would be appreciated.
Thanks,
Tim
There are several ways to do this, Using the $parse service may be the easiest
var parser = $parse("name");
var element = {name:"thingA"};
var x = parser(element);
console.log(x); // "thingA"
Parse has been optimized to act quickly in these scenarios (single property look-ups). You can keep the same "parser" function around and invoke it on each element.
You could also split on the '.' and do the simple look-up yourself (reading in 'member' to your directive as a string), in simple form:
var paths = myPath.split('.');
var val = myObj;
for(var i = 0; i < paths.length; i++){
val = val[paths[i]];
}
return val;
There are also various linq-like libraries that support lambda expressions as strings (linqjs, fromjs). If you've gotta have a fat arrow function.
Your directive can look at other attributes, so you could add a property-name attribute and have your directive manually check that property. To be fancy you could use $parse like ng-repeat does to parse an expression.
<csv-list items="element in myArray" member="element.name">
Another way would be to create a 'property' filter that takes an array of objects and returns an array of property values from that object that you could use like so:
<csv-list items="myArray|property:name">
Here's what you're asking for syntactically (Show me the code - plunkr):
member="'e' |lambda: 'e.name'"
You can do this with something like (I wrote this just for the question, what I do in my apps is outlined below)
app.filter('lambda', [
'$parse',
function ($parse) {
return function (lambdaArgs, lambdaExpression, scope) {
var parsed = $parse(lambdaExpression);
var split = lambdaArgs.split(',');
var result = function () {
var args = {};
angular.extend(args, scope || {});
for (var i = 0; i < arguments.length && i < split.length; i++) {
args[split[i]] = arguments[i];
}
return parsed(args);
};
return result;
}
}
]);
Advanced usage:
(x, y, z) => x * y * z + a // a is defined on scope
'x,y,z' |lambda: 'x * y * z + a':this
The :this will pass the scope along to the lambda so it can see variables there, too. You could also pass in an aliased controller if you prefer. Note that you can also stick filters inside the first argument to the lambda filter, like:
('x'|lambda:'x | currency')(123.45) // $123.45 assuming en-US locale
HOWEVER I have thus far avoided a lambda filter in my apps by the following:
The first approach I've taken to deal with that is to use lodash-like filters.
So if I have an array of objects and your case and I want to do names, I might do:
myArray | pluck:'name'
Where pluck is a filter that looks like:
angular.module('...', [
]).filter('pluck', [
function () {
return function (collection, property) {
if (collection === undefined) {
return;
}
try {
return _.pluck(collection, property);
} catch (e) {
}
}
}
]);
I've implemented contains, every, first, keys, last, pluck, range (used like [] | range:6 for [0,1,2,3,4,5]), some, and values. You can do a lot with just those by chaining them. In all instances. I literally just wrapped the lodash library.
The second approach I've taken is to define functions inside a controller, expose them on the scope.
So in your example I'd have my controller do something like:
$scope.selectName = function (item) { return item.name };
And then have the directive accept an expression - & - and pass selectName to the expression and call the expression as a function in the directive. This is probably what the Angular team would recommend, since in-lining in the view is not easily unit-test-able (which is probably why they didn't implement lambdas). (I don't really like this, though, as sometimes (like in your case) it's strictly a presentation-thing - not a functionality-thing and should be tested in an E2E/Boundary test, not a unit test. I disagree that every little thing should be unit tested as that often times results in architecture that is (overly) complicated (imho), and E2E tests will catch the same thing. So I do not recommend this route, personally, though again I think the team would.)
3.
The third approach I've taken would be to have the directive in question accept a property-name as a string. I have an orderableList directive that does just that.

Typed literal objects in TypeScript

I have a TS definition file for Ext JS containing function store.add(a : any) (it has many overloads so I guess this is to simplify the d.ts file). I want to pass it a literal object which implements a Person interface:
interface Person
{
name: string;
age: number
}
store.add(<Person>{ name: "Sam" });
This gives me intellisense but unfortunately it is just coercing the literal into a Person, without detecting the missing field. This works as I want:
var p : Person = { name: "Sam" }; // detects a missing field
store.add(p);
But is there a way to do this without a separate variable?
I realise this would be solved with 'fixed' definition files, but I think many Javascript libraries have too many overloads to allow this. I almost need a way to dynamically overload the function definition..! Would generics help here?
Yes generics seem to be the answer. In the definition file changing:
add?( model:any ): Ext.data.IModel[];
to
add?<T>( model:T ): Ext.data.IModel[];
Allows you to call
store.add<Person>({ name: "sam" });
And it correctly shows an error!

Object-oriented models and backbone.js

Suppose I'm working with an API which returns JSON data, but which has a complex or variable structure. For example, a string-valued property may be a plain literal, or may be tagged with a language:
/* first pattern */
{ "id": 1,
"label": "a foo"
}
/* second pattern */
{ "id": 2,
"label": [ {"value": "a foo", "lang": "en"},
{"value": "un foo", "lang": "fr"}]
}
In my client-side code, I don't want to have view code worrying about whether a label is available in multiple-languages, and which one to pick, etc. Or I might want to hide the detailed JSON structure for other reasons. So, I might wrap the JSON value in an object with a suitable API:
/** Value object for foo instances sent from server */
var Foo = function( json ) {
this.json = json;
};
/** Return a suitable label for this foo object */
Foo.prototype.label = function() {
var i18n = ... ;
if (i18n.prefLang && _.isArray(this.json.label)) // ... etc etc
};
So this is all pretty normal value-object pattern, and it's helpful because it's more decoupled from the specific JSON structure, more testable, etc. OK good.
What I currently don't see a way around is how to use one of these value objects with Backbone and Marionette. Specifically, I'd like to use a Foo object as the basis for a Backbone Model, and bind it to a Marionette ItemView. However, as far as I can see, the values in a Model are taken directly from the JSON structure - I can't see a way to recognise that the objects are functions:
var modelFoo = new Backbone.Model( foo );
> undefined
modelFoo.get( "label" ).constructor
> function Function() { [native code] }
So my question is: what is a good way to decouple the attributes of a Backbone Model from the specifics of a given JSON structure, such as a complex API value? Can value objects, models and views be made to play nice?
Edit
Let me add one more example, as I think the example above focussing on i18n issues only conveys part of my concern. Simplifying somewhat, in my domain, I have waterbodies comprising rivers, lakes and inter-tidal zones. A waterbody has associated with it one or more sampling points, and each sampling point has a latest sample. This might come back from the data API on the server as something like:
{"id": "GB12345678",
"centre": {"lat": 1.2345, "long": "-2.3456"},
"type": "river",
"samplingPoints": [{"id": "sp98765",
"latestSample": {"date": "20130807",
"classification": "normal"}
}]
}
So in my view code, I could write expressions such as:
<%= waterbody.samplingPoints[0].latestSample.classification %>
or
<% if (waterbody.type === "river") { %>
but that would be horrible, and easily broken if the API format changes. Slightly better, I could abstract such manipulations out into template helper functions, but they are still hard to write tests for. What I'd like to do is have a value object class Waterbody, so that my view code can have something like:
<%= waterbody.latestClassification() %>
One of the main problems I'm finding with Marionette is the insistence on calling toJSON() on the models passed to views, but perhaps some of the computed property suggestions have a way of getting around that.
The cleanest solution IMO is to put the label accessor into the model instead of the VO:
var FooModel = Backbone.Model.extend({
getLabel : function(){
return this.getLocalized("label");
},
getLocalized : function(key){
//return correct value from "label" array
}
});
and let the views use FooModel#getLabel instead of FooModel#get("label")
--EDIT 1
This lib seems interesting for your use case as well: Backbone.Schema
It allows you to formally declare the type of your model's attributes, but also provides some syntax sugar for localized strings and allows you to create dynamic attributes (called 'computed properties'), composed from the values of other attributes.
--EDIT 2 (in response to the edited question)
IMO the VO returned from the server should be wrapped inside a model and this model is passed to the view. The model implements latestClassification, not the VO, this allows the view to directly call that method on the model.
A simple approach to this (possibly to simple for your implementation) would be to override the model's parse method to return suitable attributes:
var modelFoo = Backbone.Model.extend({
parse: function ( json ) {
var i18n = ... ;
if (i18n.prefLang && _.isArray(json.label)) {
// json.label = "complex structure"
}
return json;
}
});
That way only your model worries about how the data from the server is formatted without adding another layer of abstraction.

How to stringify JSON to JavaScript array

My form in the html DOM is a checkbox to click (there can be more than one). The problem occurs in the description string when ever I use an apostrophe, since my list object is single-quote deliniated. This is one of the checkboxes in the form:
<input type="checkbox" id="cbx" name="cbx" value="{'getPic': 'url', 'picsrc': 'http://lh3.ggpht.com/_ZB3cttqooN0/SVmJPfusGWI/AAAAAAAADvA/GuIRgh6eMOI/Grand%20Canyon%201213_121508214.JPG', 'pos': None, 'description': 'Here's what it REALLY looks like at 5:30am! Bring your headlight!'}">
The javascript that reads the values of the checked checkboxes and pushes them into an array (list):
var pylist = [];
for (i=0; i<document.picList.cbx.length; i++) {
if (document.picList.cbx[i].checked) {
pylist.push( document.picList.cbx[i].value );
}
}
var getstr = JSON.stringify(pylist);
The problem is always that getstr at this point has chopped off everthing after the single quote in the description property.
I've tried different ways of escaping it to little avail.
The problem is that the value of the checkbox already is a JSON string. One solution would be to call JSON.parse() on the value:
var pylist = [];
for (i=0; i<document.picList.cbx.length; i++) {
if (document.picList.cbx[i].checked) {
pylist.push( JSON.parse( document.picList.cbx[i].value) );
}
}
var getstr = JSON.stringify(pylist);
I've run into the same issue - we stick json in a hidden field so we don't have to query the server on every page. We replace apostrophes with a "code" before putting into the html - we added a javascript function that replaces the code with an apostrophe.
Really hacky, but it works really well. Of course, we have one place in the code that gets json from the server and one place where javascript needs to parse it - if you find you're repeating the methods throughout your code, your mileage will vary.

Resources