Within a game im currently developing, I keep all the game/save data in a big JS object.
for example:
const save = {
inventory: {
equips: {
weapon: {...},
chestplate: {...}
},
money: 50123
},
gameBooleans: {
isThisUnlocked: false,
isThatUnlocked: false
},
settings: {
version: '1.3.4'
}
}
Whenever I push out a new update, I check to see if the save.settings.version equals to the newest version. If it doesn't, I update the old save data to include the new values.
My problem right now is that as I develop the game, I add new fields to the save data. Eg. save.gameBooleans.isThisNewThingUnlocked: false.
Previously, what I have been doing is manually adding each new field to the object when the game detects an older save:
const currentVersion = '1.3.3'
if (save.settings.version === '1.3.2') {
save.gameBooleans.isThisNewThingUnlocked = false
save.gameBooleans.isThisOtherNewThingUnlocked = false
...etc
save.settings.version = currentVersion
}
This is becoming quite tedious and I sometimes miss a field which breaks the game and gets my thousands of players upset haha. I was wondering if there was a better approach.
I tried using lodash deepmerge but if I use it like this: _.mergeDeep(upToDateState, oldSave), it doesnt add in the new key/fields. And when used the other way: _.mergeDeep(oldSave, upToDateState), this adds in the new values but overwrites some existing values.
I dont want to write my own function to handle this but its starting to look like I have to. Does anyone have any ideas?
You can use the Object.keys() function to iterate through eack key of the new object and make a full copy in the save object including the new keys, withoud doing it manually. The function should be like this
function deepCopy(old_, new_) {
// Iterate through each key of the new object
Object.keys(new_).forEach(key => {
//If there is a nested object, recall the function
if (typeof new_[key] === 'object' && ! Array.isArray(new_[key]) && new_[key] !== null)
deepCopy(old_[key], new_[key])
// If there is a non-object value, just copy the value
else
old_[key] = new_[key];
});
}
Take into account that this function would make a reference copy of array values (which may be undesirable). However, you can make a new conditional to handle arrays and make a value copy using the spread operator newArray = ...oldArray.
Try it here
I created a new object called update which has 2 new keys in the gameBooleans value:
const save = {
gameBooleans: {
isThisUnlocked: false,
isThatUnlocked: false
},
settings: {
version: '1.3.4'
}
}
const update = {
gameBooleans: {
isThisUnlocked: true,
isThatUnlocked: true,
foo: true,
bar: true,
},
settings: {
version: '1.3.4'
}
}
function deepCopy(old_, new_) {
Object.keys(new_).forEach(key => {
if (typeof new_[key] === 'object' && ! Array.isArray(new_[key]) && new_[key] !== null)
deepCopy(old_[key], new_[key])
else
old_[key] = new_[key];
});
}
deepCopy(save, update)
console.log(save)
Is this enough for what you're trying to achieve?
const currentVersion = '1.3.3'
if (save.settings.version === '1.3.2') {
save = {
...save,
gameBooleans: {
...save.gameBooleans,
isThisNewThingUnlocked: false,
isThisOtherNewThingUnlocked: false
},
settings: {
...save.settings,
version: currentVersion
}
}
}
This way, you keep all the properties that you had on the previous object, adding new ones or overriding if the property already exists
Related
I want to loop a function changing the variable each time it runs until all variables are used. Currently, I have this function duplicated 20+ times changing out the "a_week" for each one.
var a_week = "yes";
var b_week = "no";
var c_week = "yes";
function onload() {
if a_week = "yes" {
document.getElementById("dot").classList.add('open');
}
else if a_week = "no" {
document.getElementById("dot").classList.add('closed');
}
}
I would make an array of your weeks, make your onload function take an argument and then loop through them.
const weeks = [true, false, false, true];
function onload(week) {
if(week) {
document.getElementById("dot").classList.add('open');
} else {
document.getElementById("dot").classList.add('closed');
}
}
weeks.forEach(onload);
You can look at the docs for forEach at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach.
For each distinct week, you may not be intending to change the same HTML element each time. The above code would just keep adding classes to the same #dot element. If you intended to target multiple elements, maybe save an array of elements or their selectors. Or combine that with the data in an object like my old answer has. You didn't share your HTML structure so I'm not sure what the best way to structure the DOM manipulation is.
Also onload is not a great name for a function since it may be assumed to be connected to the browser's onload event.
Answer for old question:
Seems like you should make whatever sa_g_week represents an object and then loop through an array of them (or make an map/object out of them if they need to be referenced by key). I'm not sure what your context is but I'm going to make up some field names and assume you're working with locations.
const locations = [
{
isOpen247: true,
isPermanentlyClosed: false,
openTime: new Date(),
closeTime: new Date(),
},
{
isOpen247: true,
isPermanentlyClosed: true,
openTime: new Date(),
closeTime: new Date(),
},
];
You could then loop through the array and do what you need to:
locations.forEach(location => {
if(location.isOpen247) {
// do stuff
} else {
// do other stuff
}
});
JS-Interpreter is a somewhat well-known JavaScript Interpreter. It has security advantages in that it can completely isolate your code from document and allows you to detect attacks such as infinite loops and memory bombs. This allows you to run externally defined code safely.
I have an object, say o like this:
let o = {
hidden: null,
regex: null,
process: [
"this.hidden = !this.visible;",
"this.regex = new RegExp(this.validate, 'i');"
],
visible: true,
validate: "^[a-z]+$"
};
I'd like to be able to run the code in process through JS-Interpreter:
for (let i = 0; i < o.process.length; i++)
interpretWithinContext(o, o.process[i]);
Where interpretWithinContext will create an interpreter using the first argument as the context, i.e. o becomes this, and the second argument is the line of code to run. After running the above code, I would expect o to be:
{
hidden: false,
regex: /^[a-z]+$/i,
process: [
"this.hidden = !this.visible;",
"this.regex = new RegExp(this.validate, 'i');"
],
visible: true,
validate: '^[a-z]+$'
}
That is, hidden and regex are now set.
Does anyone know if this is possible in JS-Interpreter?
I’ve spent a while messing around with the JS-Interpreter now, trying to figure out from the source how to place an object into the interpreter’s scope that can be both read and modified.
Unfortunately, the way this library is built, all the useful internal things are minified so we cannot really utilize the internal things and just put an object inside. Attempts to add a proxy object also failed failed since the object just wasn’t used in a “normal” way.
So my original approach to this was to just fall back to providing simple utility functions to access the outside object. This is fully supported by the library and probably the safest way of interacting with it. It does require you to change the process code though, in order to use those functions. But as a benefit, it does provide a very clean interface to communicate with “the outside world”. You can find the solution for this in the following hidden snippet:
function createInterpreter (dataObj) {
function initialize (intp, scope) {
intp.setProperty(scope, 'get', intp.createNativeFunction(function (prop) {
return intp.nativeToPseudo(dataObj[prop]);
}), intp.READONLY_DESCRIPTOR);
intp.setProperty(scope, 'set', intp.createNativeFunction(function (prop, value) {
dataObj[prop] = intp.pseudoToNative(value);
}), intp.READONLY_DESCRIPTOR);
}
return function (code) {
const interpreter = new Interpreter(code, initialize);
interpreter.run();
return interpreter.value;
};
}
let o = {
hidden: null,
regex: null,
process: [
"set('hidden', !get('visible'));",
"set('regex', new RegExp(get('validate'), 'i'));"
],
visible: true,
validate: "^[a-z]+$"
};
const interprete = createInterpreter(o);
for (const process of o.process) {
interprete(process);
}
console.log(o.hidden); // false
console.log(o.regex); // /^[a-z]+$/i
<script src="https://neil.fraser.name/software/JS-Interpreter/acorn_interpreter.js"></script>
However, after posting above solution, I just couldn’t stop thinking about this, so I dug deeper. As I learned, the methods getProperty and setProperty are not just used to set up the initial sandbox scope, but also as the code is being interpreted. So we can use this to create a proxy-like behavior for our object.
My solution here is based on code I found in an issue comment about doing this by modifying the Interpreter type. Unfortunately, the code is written in CoffeeScript and also based on some older versions, so we cannot use it exactly as it is. There’s also still the problem of the internals being minified, which we’ll get to in a moment.
The overall idea is to introduce a “connected object” into the scope which we will handle as a special case inside the getProperty and setProperty to map to our actual object.
But for that, we need to overwrite those two methods which is a problem because they are minified and received different internal names. Fortunately, the end of the source contains the following:
// Preserve top-level API functions from being pruned/renamed by JS compilers.
// …
Interpreter.prototype['getProperty'] = Interpreter.prototype.getProperty;
Interpreter.prototype['setProperty'] = Interpreter.prototype.setProperty;
So even if a minifier mangles the names on the right, it won’t touch the ones on the left. So that’s how the author made particular functions available for public use. But we want to overwrite them, so we cannot just overwrite the friendly names, we also need to replace the minified copies! But since we have a way to access the functions, we can also search for any other copy of them with a mangled name.
So that’s what I’m doing in my solution at the beginning in patchInterpreter: Define the new methods we’ll overwrite the existing ones with. Then, look for all the names (mangled or not) that refer to those functions, and replace them all with the new definition.
In the end, after patching the Interpreter, we just need to add a connected object into the scope. We cannot use the name this since that’s already used, but we can just choose something else, for example o:
function patchInterpreter (Interpreter) {
const originalGetProperty = Interpreter.prototype.getProperty;
const originalSetProperty = Interpreter.prototype.setProperty;
function newGetProperty(obj, name) {
if (obj == null || !obj._connected) {
return originalGetProperty.call(this, obj, name);
}
const value = obj._connected[name];
if (typeof value === 'object') {
// if the value is an object itself, create another connected object
return this.createConnectedObject(value);
}
return value;
}
function newSetProperty(obj, name, value, opt_descriptor) {
if (obj == null || !obj._connected) {
return originalSetProperty.call(this, obj, name, value, opt_descriptor);
}
obj._connected[name] = this.pseudoToNative(value);
}
let getKeys = [];
let setKeys = [];
for (const key of Object.keys(Interpreter.prototype)) {
if (Interpreter.prototype[key] === originalGetProperty) {
getKeys.push(key);
}
if (Interpreter.prototype[key] === originalSetProperty) {
setKeys.push(key);
}
}
for (const key of getKeys) {
Interpreter.prototype[key] = newGetProperty;
}
for (const key of setKeys) {
Interpreter.prototype[key] = newSetProperty;
}
Interpreter.prototype.createConnectedObject = function (obj) {
const connectedObject = this.createObject(this.OBJECT);
connectedObject._connected = obj;
return connectedObject;
};
}
patchInterpreter(Interpreter);
// actual application code
function createInterpreter (dataObj) {
function initialize (intp, scope) {
// add a connected object for `dataObj`
intp.setProperty(scope, 'o', intp.createConnectedObject(dataObj), intp.READONLY_DESCRIPTOR);
}
return function (code) {
const interpreter = new Interpreter(code, initialize);
interpreter.run();
return interpreter.value;
};
}
let o = {
hidden: null,
regex: null,
process: [
"o.hidden = !o.visible;",
"o.regex = new RegExp(o.validate, 'i');"
],
visible: true,
validate: "^[a-z]+$"
};
const interprete = createInterpreter(o);
for (const process of o.process) {
interprete(process);
}
console.log(o.hidden); // false
console.log(o.regex); // /^[a-z]+$/i
<script src="https://neil.fraser.name/software/JS-Interpreter/acorn_interpreter.js"></script>
And that’s it! Note that while that new implementation does already work with nested objects, it may not work with every type. So you should probably be careful what kind of objects you pass into the sandbox. It’s probably a good idea to create separate and explicitly safe objects with only basic or primitive types.
Have not tried JS-Interpreter. You can use new Function() and Function.prototype.call() to achieve requirement
let o = {
hidden: null,
regex: null,
process: [
"this.hidden = !this.visible;",
"this.regex = new RegExp(this.validate, 'i');"
],
visible: true,
validate: "^[a-z]+$"
};
for (let i = 0; i < o.process.length; i++)
console.log(new Function(`return ${o.process[i]}`).call(o));
Hi may be interpretWithinContext look like something like that ?
let interpretWithinContext = (function(o, p){
//in dunno for what you use p because all is on object o
o.hidden = (o.hidden === null) ? false : o.hidden;
o.regex = (o.regex === null) ? '/^[a-z]+$/i' : o.regex;
console.log(o);
return o;
});
https://codepen.io/anon/pen/oGwyra?editors=1111
I ended up with the following code while experimenting with trying to detect a context update. Here is the code snippet, retyped by hand so forgive obvious typos
componentWillReceiveProps(nextProps) {
if(!this.state.myChart || this.state.myChart.options.title.fontColor != this.context.muiTheme.palete.secondaryTextColor){
const options = this.generateOptions(nextProps.options);
this.state.myChart=new Chart(document.getElementById(nextProps.canvasId),
type: nextProps.type,
data: nextProps.data,
options: options
});
} else{
//updates data and calls this.state.mychart.upddate()
}
}
generateOptions(nextProps){
const defaultOptions = chartStyles.get(nextProps.style) || Map({}); //chartStyles holds Immutable.Map maps of various default option styles
const optionsWithTitle= this.addTitle(defaultOptions, nextProps.title);
return optionsWTitle.mergeDeep(nextProps.options.toJs();
}
addTitle(options, title){
return options.mergeDeep({
title: {
text: title,
fontColor: this.context.muiTheme.palette.secondaryTextColor
}
})
}
I receive the error cyclic object value.
Experimentation shows the issue is in my if statement comparing mychart.options.title.fontcolor to muiTheme. if I change either side of the comparison it works.
What is it about the string comparison that could cause a cyclic object?
I think your issue is in setting the state. You should be using setState() method there. Also, be sure to use strict equality in your conditionals (!= vs !==)
componentWillReceiveProps(nextProps) {
if(!this.state.myChart || this.state.myChart.options.title.fontColor !== this.context.muiTheme.palete.secondaryTextColor){
const options = this.generateOptions(nextProps.options);
const myChart = new Chart(document.getElementById(nextProps.canvasId), {
type: nextProps.type,
data: nextProps.data,
options: options
});
this.setState({myChart: myChart})
} else{
//updates data and calls this.state.myChart.update()
}
}
Let me know if this helps.
I'm currently working on a simple filemanager component which I trigger from parent component. After selecting media in the filemanager I $dispatch a simple data object with 2 keys: element & media. I use element to keep track where I want the media to be appended to my current data object and media has the media information (id, type, name and so on). This setup gives me some trouble when I want to $set the media data to variables within my data object. The variables are locales, so: nl-NL, de-NL and so on.
setMediaForPage : function(data){
if(!this.page.media[this.selectedLanguage]['id'])
{
// set for all locales
var obj = this;
this.application.locales.forEach(function(element, index, array) {
obj.$set(obj.page.media[element.locale], data.media);
})
}
else
{
// set for 1 locale
this.$set(this.page.media[this.selectedLanguage], data.media);
}
}
What happens when I run this code is that the data object shows up properly in Vue Devtools data object, but the media does not show up in the template. When I switch the language (by changing the this.selectedLanguage value), the media does show up.
I think this has to do with the variables in the object keypath, but I'm not 100% sure about that. Any thoughts on how to improve this code so I can show the selected media in the parent component without having to change the this.selectedLanguagevalue?
I don't know your data structure exactly, but you can certainly use variables as the the keypath in vue, however remember that the keyPath should be a string, not an object.
If your variable that you want to use in the keypath is part of the vue, you'd do it like this:
obj.$set('page.media[element.locale]', data.media)
... because the keyPath which is a string is intelligently parsed by Vue's $set method and is of course it knows that this path is relative to the $data object.
new Vue({
el: '#app',
data() {
return {
msg: "hello world",
attr: {
lang: {
zh: '中文',
en: 'english'
}
}
}
},
methods: {
$set2(obj, propertyName, value) {
let arr = propertyName.split('.');
let keyPath = arr.slice(0, -1).join('.');
let key = arr[arr.length - 1];
const bailRE = /[^\w.$]/
function parsePath(obj, path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
let target = parsePath(obj, keyPath);
// console.log(target, key);
// target[key] = value;
this.$set(target, key, value);
}
},
mounted() {
setTimeout(() => {
// this.$set('attr.lang.zh', '嗯');
// this.$set2(this, 'attr.lang.zh', '嗯');
this.$set2(this.attr, 'lang.zh', '嗯');
}, 1000);
}
})
调用示例:this.$set2(this.attr, 'lang.zh', '嗯');
i have also experienced similar problems,remove variables -,these variables nl-NL, de-NL change to nlNl, deNl
and i not use
obj.$set('page.media[element.locale]', data.media)
but
obj.$set('page.media.'+element.locale, data.media);
then it work
I am creating a form and I am trying to find a simple, elegant way of handling to see if all inputs exist.
Form = Ember.Object.extend({
// section 1
name: null,
age: null,
isABoolean: null,
// section 2
job: null,
numberOfSiblings: null,
isComplete: Ember.computed.and('_isSection1Complete', '_isSection2Complete'),
_isSection1Complete: function() {
var isPresent = Ember.isPresent;
return isPresent(this.get('name')) && isPresent(this.get('age')) && isPresent(this.get('isABoolean'));
}.property('name', 'age', 'isABoolean'),
_isSection2Complete: function() {
var isPresent = Ember.isPresent;
return isPresent(this.get('job')) && isPresent(this.get('numberOfSiblings'));
}.property('job', 'numberOfSiblings')
});
However, this doesn't seem to scale. My actual application will have many sections (over 20 sections).
I am looking into trying to create a re-usable computed property that fits my needs. Take for example the code of what I am going for:
Form = Ember.Object.extend({
// properties...
isComplete: Ember.computed.and('_isSection1Complete', '_isSection2Complete'),
_isSection1Complete: Ember.computed.allPresent('name', 'age', 'isABoolean'),
_isSection2Complete: Ember.computed.allPresent('job', 'numberOfSiblings')
});
I feel that this is a common case, but I'm failing to find the correct computed properties on how to execute this, so I would like to make my own.
Two questions:
Where's the best place to define the custom computed property? Can I just attach a function to Ember.computed?
Is there an easier way to solve this? I feel like I'm overlooking something simple.
As for Question #1,
You can define a custom computed helper in the App namespace. In this example, I created a new computed helper called allPresent that checks each property passed in against Ember.isPresent.
App.computed = {
allPresent: function (propertyNames) {
// copy the array
var computedArgs = propertyNames.slice(0);
computedArgs.push(function () {
return propertyNames.map(function (propertyName) {
// get the value for each property name
return this.get(propertyName);
}, this).every(Ember.isPresent);
});
return Ember.computed.apply(Ember.computed, computedArgs);
}
};
It can be used like this, per your example code:
_isSection2Complete: App.computed.allPresent(['job', 'numberOfSiblings'])
I adapted this from the approach here: http://robots.thoughtbot.com/custom-ember-computed-properties
As for Question #2, I can't think of a simpler solution.
I had to make a minor adjustment to Evan's solution, but this works perfectly for anyone else that needs it:
App.computed = {
allPresent: function () {
var propertyNames = Array.prototype.slice.call(arguments, 0);
var computedArgs = propertyNames.slice(0); // copy the array
computedArgs.push(function () {
return propertyNames.map(function (propertyName) {
// get the value for each property name
return this.get(propertyName);
}, this).every(Ember.isPresent);
});
return Ember.computed.apply(Ember.computed, computedArgs);
}
};
This can now be used as such:
_isSection2Complete: App.computed.allPresent('job', 'numberOfSiblings')