I have a code that returns a string, but since it's on render it needs to be accompanied by an HTML tag, in this case span.
I want to use this code in multiple places including placeholders and labels, but with span, I believe that's impossible.
Here's the code in question, would appreciate some ideas on how to fix.
let LocalizedString = React.createClass({
render: function() {
if (!this.getKey(loadLang, this.props.xlKey)) {
return <span>{this.props.xlKey + ' untranslated in ' + userLang + ' JSON'}</span>;
} else {
return <span>{this.getKey(loadLang, this.props.xlKey)}</span>;
}
},
getKey: function(obj, str) {
str = str.replace(/\[(\w+)\]/g, '.$1'); // let's convert indexes to properties
str = str.replace(/^\./, ''); // gets rid of leading dot
let a = str.split('.');
for (let i = 0, n = a.length; i < n; i++) {
let key = a[i];
if (key in obj) {
obj = obj[key];
} else {
return null;
}
}
return obj;
},
});
module.exports = LocalizedString;
In another file that calls LocalizedString, I have this, but makes placeholder return undefined...
<TextField alert={alerts.username} onChange={this._onChangeUsername} placeholder={<LocalizedString xlKey='pages.signin.general.username'/>} ref="username" value={this.props.username}/>
Bottom line is pretty much, can I make render return just a string without any tags or make placeholder accept and discard the span tag?
Regards.
You must have to use a function instead of a React Component as per the requirement.
React Component's render method must be bound to return single wrapped markup or essentially a Virtual DOM.
Related
I have a Javascript function declartions as a string (gotten from Function.toString), and I want to wrap all variable declarations with a function (also in Javascript), E.g.
const value = 42 to const value = wrapper(42).
First I thought of using RegEx to get the original values and location and then replace them with the wrapped value, but the RegEx got too complex very fast because of needing to think about things like multiline strings and objects. Using RegEx would also impact the ease of other people contributing to the project.
After that I looked into using a module for this, I found Acorn (used by Babel, Svelte. Parses the Javascript into an ESTree, the spec for Javascript Abstract Syntax Trees): https://github.com/acornjs/acorn, but I couldn't find a way of parsing the ESTree back to a Javascript function declaration after making the modifications.
Is there a way of parsing the ESTree back to a function, or another better solution?
You don't really need a function to stringify the tree back into code. Instead, take note of the offsets where the change should occur, and then don't apply the change in the tree, but to the original string.
Here is a demo with the acorn API:
function test () { // The function we want to tamper with
const value = 42, prefix = "prefix";
let x = 3;
for (let i = 0; i < 10; i++) {
x = (x * 997 + value) % 1000;
}
return prefix + " " + x;
}
function addInitWrappers(str) { // Returns an updated string
let ast = acorn.parse(str, {ecmaVersion: 2020});
function* iter(node) {
if (Object(node) !== node) return; // Primitive
if (node.type == "VariableDeclaration" && node.kind == "const") {
for (let {init} of node.declarations) {
yield init; // yield the offset where this initialisation occurs
}
}
for (let value of Object.values(node)) {
yield* iter(value);
}
}
// Inject the wrapper -- starting at the back
for (let {start, end} of [...iter(ast)].reverse()) {
str = str.slice(0, start) + "wrapper(" + str.slice(start, end) + ")" + str.slice(end);
}
return str;
}
function wrapper(value) { // A wrapper function to demo with
return value + 1;
}
console.log("before wrapping test() returns:", test());
let str = test.toString();
str = addInitWrappers(str);
eval(str); // Override the test function with its new definition
console.log("after wrapping test() returns:", test());
<script src="https://cdnjs.cloudflare.com/ajax/libs/acorn/8.7.1/acorn.min.js"></script>
To start off, I'm primarily an AngularJS developer and recently switched to React, and I decided to convert an angular webapp I had previously developed to a react app. Im having a bit of an issue with a component ExpressiveText that searches through a string for a match to a property on a list objects and inserts a component TriggerModal in its place that when clicked triggers a modal with more detailed information. So the properties passed into ExpressiveTest are: text, tags, and tagsProperty.
text is a string (i.e. "My search string")
tags is an array of objects (i.e. [{id: 1, name: 'my', data: {...}}, {id: 2, name: 'string', data: {...}}]
tagsProperty is the name of the property to search for as a "tag" (i.e. name)
I followed along with this issue to try and formulate an idea of how to approach this. The reason I mention that I am coming from angular is because the component I had previously created simply used something like text.replace(regex, match => <trigger-modal data={tags[i]} />) and then used angulars $compile function to render components in the text. This does not seem to be possible using react. This is what I have tried inside of my ExpressiveText component:
class ExpressiveTextComponent extends React.Component {
constructor (props) {
super(props);
this.filterText = this.filterText.bind(this);
}
filterText () {
let text = this.props.text;
this.props.tags.map(tag => {
const regex = new RegExp(`(${tag[this.props.tagsProperty]})`, 'gi');
let temp = text.split(regex);
for(let i = 1; i < temp.length; i+=2){
temp[i] = <TriggerModal data={tag} label={tag[this.props.tagsProperty]} />;
}
text = temp;
});
return text;
}
render () {
return (
<div className={this.props.className}>{this.filterText()}</div>
);
}
}
This works for the first tag. The issue with it is that once it goes to map on the second tag, text is then an array. I tried adding in a conditional to check if text is an array, but then the issue becomes that the text array becomes nested and doesnt work on the next iteration. Im having a really hard time wrapping my mind around how to handle this. I have also tried dangerouslySetInnerHTML using text.replace(...) but that doesn't work either and just renders [object Object] in place of the component. Any help or advice is much appreciated, I have to say this is probably the only major issue I have come across since my switch to React, otherwise its been very straightforward.
Edit: Since I had a question asking for expected output with a given input and more clarification, what I am looking for is a component that is given this input:
<ExpressiveText text="my text" tags={{id: 1, name: 'text'}} tagsProperty="name" />
would render
<div>my <TriggerModal label="text" data={...} /></div>
with a functional TriggerModal component.
If I am correct in my understanding of what you're trying to accomplish, this is one way to do this it. My apologies if I misunderstood your question. Also, this is pseudocode and I'll try and fill it in with real code in a bit. Sorry if this is difficult to understand, let me know and I will try to clarify
filterText () {
let text = [this.props.text];
for (let item in this.props.tags) {
//item will be something like {id: 1, name: 'text'}
let searchString = new RegExp(item.name, 'gi');
//loop through text array and see if any item matches search string regex.
while (text.some(val => val.test(searchString)) {
//if we are here, at least one item matches the regexp
//loop thru text array, and split any string by searchString, and insert <TriggerModal> in their place
for (let i = text.length-1; i >=0; i--) {
//if text[i] is string and it matches regexp, then replace with nothing
text[i].replace(searchString, "")
//insert <trigger modal>
text.splice(i, 0, <TriggerModal ... />)
}
//end of while loop - test again to see if search string still exists in test array
}
}
return text;
}
Looks like I found a solution.
filterText () {
let text = this.props.text.split(' '),
replaceIndexes = [];
if(this.props.tags.length > 0) {
this.props.tags.map(tag => {
const regex = new RegExp('(' + tag[this.props.tagsProperty] + ')', 'gi');
for(let i = 0; i < text.length; i++){
if(text[i].match(regex)){
/**
* Pretty simple if its a one-word tag, search for the word and replace.
* could potentially cause some mis-matched tags but the words
* in my usecase are pretty specific, unlikely to be used in
* normal dialogue.
*/
text[i] = <TriggerModal data={tag} label={tag[this.props.tagsLabelProperty || 'name']} />;
}else{
// for tags with spaces, split them up.
let tempTag = tag[this.props.tagsProperty].split(' ');
// check for length
if(tempTag.length > 1) {
// we will be replacing at least 1 item in the array
let replaceCount = 0,
startIndex = null;
// If the first word of tempTag matches the current index, loop through the rest of the tempTag and check to see if the next words in the text array match
if(tempTag[0].toLowerCase() === text[i].toLowerCase()){
startIndex = i;
replaceCount += 1;
// loop through temp array
for (let j = 0; j < tempTag.length; j++) {
if(tempTag[j].toLowerCase() === text[i+j].toLowerCase()){
replaceCount += 1;
}
}
// Push data into replaceIndexes array to process later to prevent errors with adjusting the indexes of the text object while looping
replaceIndexes.push({
startIndex: startIndex,
replaceCount: replaceCount,
element: <TriggerModal data={tag} label={tag[this.props.tagsLabelProperty || 'name']} />
});
}
}
}
}
});
}
// Loop through each replace index object
replaceIndexes.forEach((rep, index) => {
text.splice(rep.startIndex - index, rep.replaceCount, [rep.element, ', ']);
});
// Since we stripped out spaces, we need to put them back in the places that need them.
return text.map(item => {
if(typeof item === "string"){
return item + ' ';
}
return item;
});
}
Edit: This is actually pretty buggy. I ended up ditching my own solution in favor of this package
Say I have the following HTML:
<div class="L-shaped-icon-container">
<span class="L-shaped-icon">Something is US$50.25 and another thing is US$10.99.</span>
</div>
What I'd like to do is replace all instances of US$XX.xx with GBP£YY.yy on the live page using jquery.
The value of GBP would be determined by my own currency conversion ratio.
So I'm assuming what I'd first need to do is use a regular expression to get all instances of the prices which would be anything beginning with USD$ and ending after .xx? Prices will always have cents displayed.
Then I'm stuck what would be the best way to accomplish the next part.
Should I wrap these instances in a span tag with a class, then use jquery.each() function to loop through each and replace the contents with a jquery(this).html("GBP£YY.yy")?
Any help setting me on the right path would be greatly appreciated. Thanks guys.
base method for text replacements:
var textWalker = function (node, callback) {
var nodes = [node];
while (nodes.length > 0) {
node = nodes.shift();
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes[i];
if (child.nodeType === child.TEXT_NODE)
callback(child);
else
nodes.push(child);
}
}
};
stuff you need to do:
var USDinGBP = 0.640573954;
textWalker(document, function (n) {
n.nodeValue = n.nodeValue.replace(/(USD?\$(\d+(\.\d+)?))/g, function($0, $1, $2){
return "GBP£" + (parseFloat($2) * USDinGBP).toFixed(2);
})
})
you can fire that on ANY site. it will even replace titles etc.
so.. to tell you about the benefits of not using jquery for this:
jquery will process and wrap every single element in a browser compatible way.
using a native javascript solution would speed up this process alot.
using native textnodes also is benefitial since it will not break event handlers for child elements.
you should also consider using fastdom.
it does not matter if you are using jquery or native js. after writing to elements the dom has to do certain tasks before it can be read again. in the end you will loose some time for each edited element.
to give you a fastdom example:
var textWalker = function (node, callback) {
var nodes = [node];
while (nodes.length > 0) {
node = nodes.shift();
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes[i];
if (child.nodeType === child.TEXT_NODE)
callback(child);
else
nodes.push(child);
}
}
};
var textReplace = function (node, regex, callback) {
textWalker(node, function (n) {
fastdom.read(function () {
var text = n.nodeValue;
if (!regex.test(text)) {
return;
}
text = text.replace(regex, callback);
fastdom.write(function () {
n.nodeValue = text;
});
});
});
};
// put this function call into your onload function:
var USDinGBP = 0.640573954;
textReplace(document, /(USD?\$(\d+(\.\d+)?))/g, function($0, $1, $2){
return "GBP£" + (parseFloat($2) * USDinGBP).toFixed(2);
});
this will basically do the job in an instant.
if you want to go even further you could add this to jquery as following:
jQuery.fn.textReplace = function (regex, callback) {
this.each(function () {
textReplace(this, regex, callback);
});
};
and call it like that:
var USDinGBP = 0.640573954;
jQuery(".L-shaped-icon").textReplace(/(USD?\$(\d+(\.\d+)?))/g, function($0, $1, $2){
return "GBP£" + (parseFloat($2) * USDinGBP).toFixed(2);
});
If all of these values are directly in the span, if not you can give them a unique class and use it to iterate over them, you can use the following
You first get the numeric part of the string in a variable
convert the currency store it in other variable.
replace US$ with GBP
replace numeric part of the string with converted value
jQuery:
("span").each(function() {
var currencyVal=$(this).text().match(/\d/);
var convertedVal=currencyVal * 100; // just for example.
$(this).text($(this).text().replace(/^US$/,'GBP£'));
$(this).text($(this).text().replace(/\d/,convertedVal));
});
I hope this will helps you. Here is working Fiddle
HTML:
<div class="L-shaped-icon-container">
<span class="L-shaped-icon">Something is US$50.25 and another thing is US$10.99.</span>
<span class="L-shaped-icon">Something is BR$20.25 and another thing is US$10.99.</span>
<span class="L-shaped-icon">Something is US$10.25 and another thing is US$10.99.</span>
<span class="L-shaped-icon">Something is US$50.20 and another thing is GR$10.99.</span>
</div>
<button class="btnUpdate">Update</button>
JavaScript Code:
function UpdateCurrency(){
var UStoGB=10;
$('span').each(function(e){
var matchedText = $(this).text().match(/US\$\S+/g);
var updatedText = $(this).text();
if(matchedText){
for(var i=0;i<= matchedText.length;i++){
if(matchedText[i]){
var currentValue=matchedText[i].replace('US$','');
if(!currentValue) currentValue=0;
var newCurrency = ( parseFloat(currentValue) * UStoGB);
updatedText= updatedText.replace(matchedText[i],'GBP£'+newCurrency);
}
}
}
$(this).text(updatedText);
});
return false;
}
I'm trying to create a generic i18n solution for a HTML app I'm working in. I'm looking for alternatives to use eval() to call deeply nested Javascript objects:
Suppose the following HTML example:
<div id="page1">
<h1 data-i18n="html.pageOne.pageTitle"></h1>
</div>
and it's companion Javascript (using jQuery):
var i18n;
i18n = {
html: {
pageOne: {
pageTitle: 'Lorem Ipsum!'
}
}
};
$(document).ready(function () {
$('[data-18n]').each(function () {
var q;
q = eval('i18n.' + $(this).attr('data-i18n'));
if (q) {
$(this).text(q);
}
});
});
Any advices on how to access the "pageTitle" property inside the i18n object without using eval()? I need to keep the object's structure, so changing its layout to a "flat" solution is not feasible.
Thanks!!!
You can use bracket syntax, as others have hinted at. But, you'll need to split and iterate at .:
function lookup(obj, path) {
var keys = path.split('.'),
result = obj;
for (var i = 0, l = keys.length; i < l; i++) {
result = result[keys[i]];
// exit early if `null` or `undefined`
if (result == null)
return result;
}
return result;
}
Then:
q = lookup(i18n, $(this).attr('data-i18n'));
if (q) {
$(this).text(q);
}
The dot syntax (object.field) is really just syntactic sugar for object['field']. If you find yourself writing eval('object.'+field), you should simply write object['field'] instead. In your example above, you probably want: i18n[$(this).attr('data-i18n')].
Since you're encoding your attribute in a way that has dots in it, try splitting it by the dots, and iterating over the fields. For example (this can probably be improved):
var fields = $(this).attr('i18n').split('.');
fieldCount = fields.length;
fieldIdx = 0;
var cur = i18n;
while(cur != undefined && fieldIdx > fieldCount) {
cur = cur[fields[fieldIdx++]];
}
You'll want to do additional checking to make sure all of the fields were handled, nulls weren't encountered, etc.
You can split the string on the periods and traverse the object:
var q = i18n;
$.each($(this).attr('data-i18n').split('.'), function(index, key){
if (q) q = q[key];
});
Demo: http://jsfiddle.net/GsVsr/
I'm receiving this error using the following javascript code:
function tempTest(evt) {
alert(evt.currentTarget.id);
ct = document.getElementById(evt.currentTarget.id);
rslt = document.getElementById('rslt');
var props;
for (var prop in ct) {
if (ct.hasOwnProperty(prop)) {
propVal = ct[prop];
var propDat = prop + ' = ' + propVal;
props += propDat + '<br/>';
}
}
rslt.innerHTML = props;
}
This one has me puzzled. Any ideas?
Not all the properties of a HTML element are primitives. for example, parent, childs etc are also HTML elements. You can't just use them as strings or numbers.
You need to add there a condition and use that property accordingly.
If the object in question is json, you can call JSON.stringify(thingThatIsJson) which will return a String. .toString() does not work on json.
This is a message to those of you dealing with something like req.body which will work in console.log() which is rather confusing since it may not otherwise behave like a String (like when you're trying to add it to another String).
(The OP:)
Just wanted to post the updated snippet for anyone who stumbles onto this post...
function tempTest(evt) {
alert(evt.currentTarget.id);
ct = document.getElementById(evt.currentTarget.id);
rslt = document.getElementById('rslt');
var props;
for (var prop in ct) {
if (ct.hasOwnProperty(prop)) {
var propVal = ct[prop];
props += prop + ' (' + typeof(prop) + ')' + ' = ';
if (typeof(ct[prop]) == 'string') {
propVal += ct[prop];
} else {
if (propVal != null && propVal.toString) {
props += propVal.toString();
} else {}
}
props += '<br/>';
}
}
rslt.innerHTML = props;
}
The problem lies with the propVal part of your code. Since that may not be converted into a string.