Polymer readOnly access private setter for given property - javascript

I am aware you can set a readOnly property by using the generated setter: _setMyProp(newValue)
But what if I don't know the name of the property before-hand? Is there any way to access the setter for a given property? My current solution is mapping property names to their setter functions:
_setters: {
type: Object,
value: {
prop1 : function(newVal) { this._setProp1(newVal); },
prop2 : function(newVal) { this._setProp2(newVal); }
}
}
Is there a more elegant way to do this?
Update
For more context, this is the function that calls _setters. This function should ideally not use any actual property names. This is what I meant by "don't know the name of the property before-hand"
load: function(data, key) {
if (!_.isEmpty(data)) {
if (key) {
this._setters[key].bind(this)(data);
} else {
_.each(this._setters, function(setter, key) {
setter.bind(this)(data[key]);
}.bind(this));
}
}
},
The element behaves like iron-meta and loads data from a global object when it's ready, and sets all it's properties.
ready: function() {
this.load(_data);
},
But it must also load specific properties when an ajax response comes in, for each attached element.
_els.forEach(function(el) {
el.load(key, _data[key]);
});
I'm trying to make it as easy as possible to define more properties later on. Polymer's this.set('propName', 'value'); would be perfect but does not work for readOnly properties.
If I should just scrap this entirely and go a different route, I'd also be open to that advice.
Update 2
My solution for lack of a better one was to abandon readOnly and just use this.set
This is what load looks like now:
load: function(data, property) {
if (!_.isEmpty(data)) {
if (property) {
this.set(property, data[property]);
} else {
Object.keys(this.properties).forEach(function(property) {
this.set(property, data[property]);
}.bind(this));
}
}
},

Related

Why I shouldn't set data from computed?

Using vuex, I receive an object, lets suppose
user: {name: 'test'}
And in app.vue I use this.$store.getters.user
computed: {
user: function() {
let user = this.$store.getters.user
return user
}
}
While setting also data object 'this.name'
data() {
return {
name: ''
}
}
computed: {
user: function() {
let user = this.$store.getters.user
this.name = user.name
return user
}
}
But in the lint I get this error 'unexpected side effect in computed property', (the data 'name' should be used as a v-model, to be used as a update API parameter).
I know it can be ignored if you know what you're doing, and that it is triggered for setting data from computed, but why it triggers this error? and how to workaround it?
don't set value in computed. if you need to get name of computed user you must be create new computed:
user: function() {
let user = this.$store.getters.user
return user
},
name: function() {
if(this.user.name!=undefined) return this.user.name
return ''
},
and remove name from data
but if you realy need to set name you can watch user and set name
watch: {
user(newVal) {
if(newVal.name!=undefined) this.name = newVal.name
}
}
Vue has both computed getters and setters. If you define a computed property as you did above it is only a getter. A getter is meant to only "get" a value and it should be a "pure" function with no side effects so that it is easier to read, debug and test.
From the docs for the rule that triggered the linting error on your code:
It is considered a very bad practice to introduce side effects inside
computed properties. It makes the code not predictable and hard to
understand.
In your case you might want to use a computed setter for either the user or the name values so that you can use them as v-models. You could, for example, do:
computed: {
user: function () {
return this.$store.getters.user;
},
user: {
// getter
get: function () {
return this.user.name;
},
// setter
set: function (newValue) {
this.$store.commit('setUserName', newValue);
}
}
}

Vuex: Can't change deeply nested state data inside actions

In the store, I have an action to update some data, the action looks like this:
setRoomImage({ state }, { room, index, subIndex, image }) {
state.fullReport.rooms[room].items[index].items[subIndex].image = image;
console.log(state.fullReport.rooms[room].items[index].items[subIndex])
},
Because all of this data is dynamic so I have to dynamically change the nested values and can't directly hard code the properties.
The data looks like this:
fullreport: {
rooms: {
abc: {
items: [
{
type: "image-only",
items: [
{
label: "Main Image 1",
image: ""
},
{
label: "Main Image 2",
image: ""
}
]
}
]
}
}
}
When I dispatch the action, In the console I can see that the value of the sub-property image is successfully mutated, but if I access the VueX store from the Vue DevTools inside Chrome, I see that value doesn't change there. Here is the console output:
Please, can somebody tell why is it happening? As I know that data is successfully changing, but somehow the state isn't showing it and hence my components do not rerender.
I also tried using Vue.set instead of simple assignment, but still no luck :(
Vue.set(
state.fullReport.rooms[room].items[index].items[subIndex],
"image",
image
);
Edit:
Following David Gard's answer, I tried the following:
I am also using Lodash _ (I know making whole copies of objects isn't good), this is the mutation code block.
let fullReportCopy = _.cloneDeep(state.fullReport);
fullReportCopy.rooms[room].items[index].items[subIndex].image = image;
Vue.set(state, "fullReport", fullReportCopy);
Now In the computed property, where the state.fullReport is a dependency, I have a console.log which just prints out a string whenever the computed property is re-computed.
Every time I commit this mutation, I see the computed property logs the string, but the state it is receiving still doesn't change, I guess Vue.set just tells the computed property that the state is changed, but it doesn't actually change it. Hence there is no change in my component's UI.
As mentioned in comments - it quickly gets complicated if you hold deeply nested state in your store.
The issue is, that you have to fill Arrays and Objects in two different ways, hence, consider whether you need access to their native methods or not. Unfortunately Vuex does not support reactive Maps yet.
That aside, I also work with projects that require the dynamic setting of properties with multiple nested levels. One way to go about it is to recursively set each property.
It's not pretty, but it works:
function createReactiveNestedObject(rootProp, object) {
// root is your rootProperty; e.g. state.fullReport
// object is the entire nested object you want to set
let root = rootProp;
const isArray = root instanceof Array;
// you need to fill Arrays with native Array methods (.push())
// and Object with Vue.set()
Object.keys(object).forEach((key, i) => {
if (object[key] instanceof Array) {
createReactiveArray(isArray, root, key, object[key])
} else if (object[key] instanceof Object) {
createReactiveObject(isArray, root, key, object[key]);
} else {
setReactiveValue(isArray, root, key, object[key])
}
})
}
function createReactiveArray(isArray, root, key, values) {
if (isArray) {
root.push([]);
} else {
Vue.set(root, key, []);
}
fillArray(root[key], values)
}
function fillArray(rootArray, arrayElements) {
arrayElements.forEach((element, i) => {
if (element instanceof Array) {
rootArray.push([])
} else if (element instanceof Object) {
rootArray.push({});
} else {
rootArray.push(element);
}
createReactiveNestedFilterObject(rootArray[i], element);
})
}
function createReactiveObject(isArray, obj, key, values) {
if (isArray) {
obj.push({});
} else {
Vue.set(obj, key, {});
}
createReactiveNestedFilterObject(obj[key], values);
}
function setValue(isArray, obj, key, value) {
if (isArray) {
obj.push(value);
} else {
Vue.set(obj, key, value);
}
}
If someone has a smarter way to do this I am very keen to hear it!
Edit:
The way I use the above posted solution is like this:
// in store/actions.js
export const actions = {
...
async prepareReactiveObject({ commit }, rawObject) {
commit('CREATE_REACTIVE_OBJECT', rawObject);
},
...
}
// in store/mutations.js
import { helper } from './helpers';
export const mutations = {
...
CREATE_REACTIVE_OBJECT(state, rawObject) {
helper.createReactiveNestedObject(state.rootProperty, rawObject);
},
...
}
// in store/helper.js
// the above functions and
export const helper = {
createReactiveNestedObject
}
Excluding the good practices on the comments.
That you need is: to instruct Vue when the object change (Complex objects are not reactive). Use Vue.set. You need to set the entire object:
Vue.set(
state,
"fullReport",
state.fullReport
);
Documentation: https://v2.vuejs.org/v2/api/#Vue-set

Using a proxy to access the dataset of a custom element

Imagine this trivial custom element:
<my-el data-cute-number="7" id="foo"></my-el>
document.getElementById('foo').dataset.cuteNumber, as expected, returns the String "7". I would like to create a proxy for accessing dataset properties that does the casting to Number for me because I'm using the property alot in the component code and would like to avoid repeatedly having to cast it manually every time I access it. I also do not want to create an additional getter for a new property (e.g. get cuteNumber() { return Number(this.dataset.cuteNumber); }) on the component itself since I will have to do all the synchronisation manually then (since I'd also need a setter), make sure I avoid infinite update loops, etc.
As I understand proxies, this is exactly where a proxy will help me.
customElements.define('my-el', class extends HTMLElement {
constructor() {
super();
const proxy = new Proxy(this.dataset, {
get: function(context, prop, receiver) {
console.log(`Proxy getter executing for ${prop}`);
switch (prop) {
case 'cuteNumber':
return Number(context[prop]);
break;
default:
return context[prop];
}
}
});
}
})
console.log(typeof document.getElementById('foo').dataset.cuteNumber);
<my-el data-cute-number="7" id="foo"></my-el>
This is where I'm stuck.
Accessing the dataset currently does not trigger the proxy (the inner console.log doesn't show up).
Can anyone point me in the right direction? Is it even possible to proxy the dataset of an element?
Creating a proxy does not mutate the target object, it doesn't become a proxy. The proxy is a new object that wraps around the target. In your code, you are just throwing away the proxy and never use it - the .dataset property is unaffected. You'll want to either overwrite it or create a new property:
customElements.define('my-el', class extends HTMLElement {
get dataset() {
//^^^^^^^^^^^^^
return new Proxy(super.dataset, {
// ^^^^^^
get: function(target, prop, receiver) {
console.log(`Proxy getter executing for ${prop}`);
if (prop == 'cuteNumber')
return Number(target.cuteNumber);
return Reflect.get(target, prop, receiver);
}
});
}
});
console.log(typeof document.getElementById('foo').dataset.cuteNumber);
<my-el data-cute-number="7" id="foo"></my-el>
customElements.define('my-el', class extends HTMLElement {
constructor() {
super();
this.numberdata = new Proxy(this.dataset, {
// ^^^^^^^^^^^^^^^^^
get: function(target, prop, receiver) {
return Number(target[prop]);
}
});
}
});
console.log(typeof document.getElementById('foo').numberdata.cuteNumber);
<my-el data-cute-number="7" id="foo"></my-el>

Is it possible to pass a function on input to another component in Polymer

I want to refactor my code on smart and dumb components (logic and presentation components) and I'm wondering if it's possible to pass function in input and trigger it from the host component (as in Angular for example).
It is possible. I took this approach to have a component for model data and a separate filtering component with toggles that would pass a function to the model component via a propery like so:
<my-model id="modelData" items="{{items}}" filters="{{filters}}"></my-model>
<my-filter id="listFilter" active-filter="{{filters}}"></my-filter>
inside <my-filter>:
static get properties() {
return {
activeFilter: {
type: Object,
notify: true
}
}
}
apply() {
this.activeFilter = this._compute(this.$.myToggle.value)
}
_compute(toggle) {
return function(item) {
return item.condition == toggle
}
}
inside <my-model>:
_filterModel(model, filters) {
for (let item of model) {
if (filters(item)) {
this.push('filteredList', item)
}
}
}
Although I made it this way just as a proof of concept, as normally I would just pass around the object to be modified.

VueJs - bind input to url param

I would like to bind an input field to a route param in vue.js.
<input v-model="$route.params.query"/>
Is it possible?
The most straight-forward way I found was the following:
<input v-model="foo" />
--
data() {
return {
foo: this.$route.query.foo
};
},
watch: {
foo(newVal) {
this.$router.push({ query: { ...this.$route.query, foo: newVal } });
},
'$route.query.foo': function(val) {
this.foo = val;
}
}
Edit 2019-08-16: added watch for $route to react to back navigation.
A bit of time has passed, but since the docs say "it is often a better idea to use a computed property rather than an imperative watch callback" I thought I would add this pattern which I tested successfully and also looks more elegant IMHO.
The idea is to use a computed property linked to the query param with explicit get and set methods and bind the input with v-model to that property.
So in your template:
<input v-model="query_param"/>
Then in your computed properties:
computed: {
query_param: {
get() {
return this.$route.query.your_query_param
},
set(value) {
/* Here I use replace so that you're not actually
pushing a new page to the history stack */
this.$router.replace({
query: {
...this.$route.query,
your_query_param: value
}
})
}
}
}
This of course would work for a single query parameter. If you have multiple inputs you want to bind to different parameters just put additional computed properties in the same way.
Yes, it is possible. In my code, I have a <select> element on change of which I want to append the selected value to url as query. So, I have done like this:
My previous URL was:
localhost:8080/books
<select class="form-control" v-on:change="appendQueryToUrl" v-model="filterData.genre">
methods: {
appendQueryToUrl() {
this.$router.push({query: {genre: this.filterData.genre}})
}
}
Where genre is my key and this.filterData.genre is my local object value. Now my url looks like:
localhost:8080/books/?genre=Computer
Thanks and inform me if any error.

Categories

Resources