_.defaultsDeep() with Vue.set() - vue.js

I need to set default values from model to component object in my vue.js application.
I found the perfect solution in lodash defaultsDeep defaultsDeep(this.widget, this.widgetModel), but the values don't get reactive obviously (added props not reactive), so I need something similar to _.defaultsDeep(), but with a callback to vm.$set() OR make all properties of object reactive after set defaults, OR even add defaultsDeepWith function to lodash
I looked to source code of defaultsDeep, but looks like i don't have enough experience to understand that, also i looked to vue-deepset librariy and seems it don't fit to my case (library better fit to stringed properties), also project based on vue.js 2

const defaultsDeepWithSet = (targetObj, sourceObj) => {
for (let prop in sourceObj) {
if (sourceObj.hasOwnProperty(prop)) {
if (!targetObj.hasOwnProperty(prop)) {
this.$set(targetObj, prop, sourceObj[prop])
}
if (isObject(sourceObj[prop])) {
defaultsDeepWithSet(targetObj[prop], sourceObj[prop])
}
}
}
}
defaultsDeepWithSet(this.widget, this.widgetModel)
Obama awards obama a medal meme

I've run into this same issue with _.defaultsDeep() and other similar utils. If this is a prominent issue and you don't want Vue to bleed into every corner of your codebase, then consider fixing reactivity after the fact.
I happen to be using Vue 2 Composition API, so it looks like this:
import { reactive, isReactive, isRaw } from '#vue/composition-api';
const fixReactivity = (obj) => {
// Done?
if (!obj) return obj;
if (typeof obj !== 'object') return obj;
// Force reactive
if (!isReactive(obj)) {
// See: https://github.com/vuejs/composition-api/blob/6247ba3a1e593b297321f68c1b7bb0dee2c3ea1e/src/reactivity/reactive.ts#L43
const canBeReactive = !(!(Object.prototype.toString.call(obj) === '[object Object]' || Array.isArray(obj)) || isRaw(obj) || !Object.isExtensible(obj));
if (canBeReactive) reactive(obj);
}
const isArray = Array.isArray(obj);
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
// Fix the children
const val = obj[key];
fixReactivity(val);
// Fix assignment for objects
if (!isArray) {
const prop = Object.getOwnPropertyDescriptor(obj, key);
const needsFix = ('value' in prop) && prop.writable;
if (needsFix) {
Vue.delete(obj, key);
Vue.set(obj, key, val);
}
}
}
return obj;
};

Related

Simulate v-if directive in custom directive

I need to destroy element in custom directive like v-if. (Forbid item creation if the condition fails.)
I trying this
export const moduleDirective: DirectiveOptions | DirectiveFunction = (el, binding, vnode) => {
const moduleStatus = store.getters[`permissions/${binding.value}Enabled`];
if (!moduleStatus) {
const comment = document.createComment(' ');
Object.defineProperty(comment, 'setAttribute', {
value: () => undefined,
});
vnode.elm = comment;
vnode.text = ' ';
vnode.isComment = true;
vnode.context = undefined;
vnode.tag = undefined;
if (el.parentNode) {
el.parentNode.replaceChild(comment, el);
}
}
};
But this option does not suit me. It does not interrupt the creation of the component.
this code removes an element from DOM, but not destroy a component instance.
I'm not checked this but from doc it should look like this.
probably I will edit it later with a more specific and correct answer
Vue.directive('condition', {
inserted(el, binding, vnode, oldVnode){
/* called when the bound element has been inserted into its parent node
(this only guarantees parent node presence, not necessarily in-document). */
if (!test){ el.remove() }
}
update(el, binding, vnode, oldVnode ){
/* called after the containing component’s VNode has updated, but possibly before
its children have updated. */
if (!test()){ el.remove() }
//or you can try this, changed the vnode
vnode.props.vIf = test();
}
}
Or in short
Vue.directive('condition', function (el, binding) {
if (!test){ el.remove() }
})
Apart from el, you should treat these arguments as read-only and never modify them. If you need to share information across hooks, it is recommended to do so through element’s dataset.

Can't get data of computed state from store - Vue

I'm learning Vue and have been struggling to get the data from a computed property. I am retrieving comments from the store and them processing through a function called chunkify() however I'm getting the following error.
Despite the comments being computed correctly.
What am I doing wrong here? Any help would be greatly appreciated.
Home.vue
export default {
name: 'Home',
computed: {
comments() {
return this.$store.state.comments
},
},
methods: {
init() {
const comments = this.chunkify(this.comments, 3);
comments[0] = this.chunkify(comments[0], 3);
comments[1] = this.chunkify(comments[1], 3);
comments[2] = this.chunkify(comments[2], 3);
console.log(comments)
},
chunkify(a, n) {
if (n < 2)
return [a];
const len = a.length;
const out = [];
let i = 0;
let size;
if (len % n === 0) {
size = Math.floor(len / n);
while (i < len) {
out.push(a.slice(i, i += size));
}
} else {
while (i < len) {
size = Math.ceil((len - i) / n--);
out.push(a.slice(i, i += size));
}
}
return out;
},
},
mounted() {
this.init()
}
}
Like I wrote in the comments, the OPs problem is that he's accessing a store property that is not available (probably waiting on an AJAX request to come in) when the component is mounted.
Instead of eagerly assuming the data is present when the component is mounted, I suggested that the store property be watched and this.init() called when the propery is loaded.
However, I think this may not be the right approach, since the watch method will be called every time the property changes, which is not semantic for the case of doing prep work on data. I can suggest two solutions that I think are more elegant.
1. Trigger an event when the data is loaded
It's easy to set up a global messaging bus in Vue (see, for example, this post).
Assuming that the property is being loaded in a Vuex action,the flow would be similar to:
{
...
actions: {
async comments() {
try {
await loadComments()
EventBus.trigger("comments:load:success")
} catch (e) {
EventBus.trigger("comments:load:error", e)
}
}
}
...
}
You can gripe a bit about reactivity and events going agains the reactive philosophy. But this may be an example of a case where events are just more semantic.
2. The reactive approach
I try to keep computation outside of my views. Instead of defining chunkify inside your component, you can instead tie that in to your store.
So, say that I have a JavaScrip module called store that exports the Vuex store. I would define chunkify as a named function in that module
function chunkify (a, n) {
...
}
(This can be defined at the bottom of the JS module, for readability, thanks to function hoisting.)
Then, in your store definition,
const store = new Vuex.Store({
state: { ... },
...
getters: {
chunkedComments (state) {
return function (chunks) {
if (state.comments)
return chunkify(state.comments, chunks);
return state.comments
}
}
}
...
})
In your component, the computed prop would now be
computed: {
comments() {
return this.$store.getters.chunkedComments(3);
},
}
Then the update cascase will flow from the getter, which will update when comments are retrieved, which will update the component's computed prop, which will update the ui.
Use getters, merge chuckify and init function inside the getter.And for computed comment function will return this.$store.getters.YOURFUNC (merge of chuckify and init function). do not add anything inside mounted.

Defining user-defined getters in a Vue component

My question relates to Vue and more specifically reactivity and reactive getter/setters: https://v2.vuejs.org/v2/guide/reactivity.html
Can I define my own getters in a Vue component and what will happen to them when Vue add its own reactive getters?
Vue will walk through all of its properties and convert them to getter/setters using Object.defineProperty
What the above sentence means is vue iterates over each property in your data option to make them reactive.
For example consider your data option to be:
data:{
foo: 'hello world',
bar: 3
}
vue will override the data object as follows(just an abstract description):
let key = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
let val = data[keys[i]];
Object.defineProperty(data, keys[i], {
get(){
// add this property as a dependency when accessed
return val;
},
set(newVal){
//notify for a change
val = newVal;
}
})
}
If you check out the vue source code for the same you will find that it checks whether the properties have predefined getters or setters.
Then it overrides the properties getter as follows:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value;
},
set(newVal) {
//...
}
});
If you see this line const value = getter ? getter.call(obj) : val;
you'll notice that if you defined a getter it is being used and is its value returned.
Vue is just doing some more work to make them reactive by invoking some dependency related methods thats it.

How to remove attributes from tags inside Vue components?

I want to use data-test attributes (as suggested here), so when running tests I can reference tags using these attributes.
<template>
<div class="card-deck" data-test="container">content</div>
</template>
The element will be found using:
document.querySelector('[data-test="container"]')
// and not using: document.querySelector('.card-deck')
But I don't want these data-test attributes to get to production, I need to remove them. How can I do that? (There are babel plugins that do that for react.)
Thanks!
The solution (for a Nuxt.js project), provided by Linus Borg in this thread, is:
// nuxt.config.js
module.exports = {
// ...
build: {
// ...
extend(config, ctx) {
// ...
const vueLoader = config.module.rules.find(rule => rule.loader === 'vue-loader')
vueLoader.options.compilerModules = [{
preTransformNode(astEl) {
if (!ctx.dev) {
const {attrsMap, attrsList} = astEl
tagAttributesForTesting.forEach((attribute) => {
if (attrsMap[attribute]) {
delete attrsMap[attribute]
const index = attrsList.findIndex(x => x.name === attribute)
attrsList.splice(index, 1)
}
})
}
return astEl
},
}]
}
}
}
where tagAttributesForTesting is an array with all attributes to be removed, like: ["data-test", ":data-test", "v-bind:data-test"].
For those of you who want to know how to do this is vanilla Vue 3, read on.
According to the Vue CLI documentation, the correct way to override a loader's options is to use the chainWebpack method within your Vue configuration (within your vue.config.js file):
module.exports = {
chainWebpack(config) {
config.module
.rule('vue')
.use('vue-loader')
.tap((options) => {
options.compilerOptions.modules = [{
preTransformNode(element) {
if (process.env.NODE_ENV !== 'production') return
const { attrsMap, attrsList } = element;
if ('data-test' in attrsMap) {
delete attrsMap[attribute];
const index = attrsList.findIndex(x => x.name === attribute);
attrsList.splice(index, 1)
}
return element;
}
}];
return options;
});
}
};
For your particular use case, I think the most maintenance free option would be to use a pattern matching strategy to remove test attributes. This would keep you from having to add every new test attribute to the list of blacklisted attributes:
{
preTransformNode(element) {
if (process.env.NODE_ENV !== 'production') return
const { attrsMap, attrsList } = element;
for (const attribute in attrsMap) {
// For example, you could add a unique prefix to all of your test
// attributes (e.g. "data-test-***") and then check for that prefix
// using a Regular Expression
if (/^data-test/.test(attribute)) {
delete attrsMap[attribute];
const index = attrsList.findIndex(x => x.name === attribute);
attrsList.splice(index, 1)
}
}
return element;
}
}
Note that the attributes will include any Vue directives (e.g. "v-bind:") that you attach to them, so be sure to compensate for that if you decide to identify your test attributes using unique prefixes.
I think it would be best to mention that, just like #ahwo before me, I drew my inspiration from Linus Borg's suggestion on the Vue forums.
P.s. With Vue, it's possible to create attributes that have dynamic names. I think this would be useful to know for anyone who is adding attributes for testing

Access an element's Binding

I have a custom attribute that processes authentication data and does some fun stuff based on the instructions.
<div auth="disabled: abc; show: xyz; highlight: 123">
There's a lot of complicated, delicate stuff happening in here and it makes sense to keep it separate from semantic bindings like disabled.bind. However, some elements will have application-logic level bindings as well.
<div auth="disabled.bind: canEdit" disabled.bind="!editing">
Under the covers, my auth attribute looks at the logged in user, determines if the user has the correct permissions, and takes the correct action based on the result.
disabledChanged(value) {
const isDisabled = this.checkPermissions(value);
if (isDisabled) {
this.element.disabled = true;
}
}
This result needs to override other bindings, which may or may not exist. Ideally, I'd like to look for an existing Binding and override it ala binding behaviors.
constructor(element) {
const bindings = this.getBindings(element); // What is the getBindings() function?
const method = bindings['disabled']
if (method) {
bindings['disabled'] = () => this.checkPermission(this.value) && method();
}
}
The question is what is this getBindings(element) function? How can I access arbitrary bindings on an element?
Edit: Gist here: https://gist.run/?id=4f2879410506c7da3b9354af3bcf2fa1
The disabled attribute is just an element attribute, so you can simply use the built in APIs to do this. Check out a runnable example here: https://gist.run/?id=b7fef34ea5871dcf1a23bae4afaa9dde
Using setAttribute and removeAttribute (since the disabled attribute does not really have a value, its mere existence causes the element to be disabled), is all that needs to happen:
import {inject} from 'aurelia-framework';
#inject(Element)
export class AuthCustomAttribute {
constructor(element) {
this.el = element;
}
attached() {
let val = false;
setInterval(() => {
if(this.val) {
this.el.setAttribute('disabled', 'disabled');
} else {
this.el.removeAttribute('disabled');
}
this.val = !this.val;
}, 1000);
}
}
NEW RESPONSE BELOW
You need to work directly with the binding engine. A runnable gist is located here: https://gist.run/?id=b7fef34ea5871dcf1a23bae4afaa9dde
Basically, you need to get the original binding expression, cache it, and then replace it (if auth === false) with a binding expression of true. Then you need to unbind and rebind the binding expression:
import {inject} from 'aurelia-framework';
import {Parser} from 'aurelia-binding';
#inject(Element, Parser)
export class AuthCustomAttribute {
constructor(element, parser) {
this.el = element;
this.parser = parser;
}
created(owningView) {
this.disabledBinding = owningView.bindings.find( b => b.target === this.el && b.targetProperty === 'disabled');
if( this.disabledBinding ) {
this.disabledBinding.originalSourceExpression = this.disabledBinding.sourceExpression;
// this expression will always evaluate to true
this.expression = this.parser.parse('true');
}
}
bind() {
// for some reason if I don't do this, then valueChanged is getting called before created
this.valueChanged();
}
unbind() {
if(this.disabledBinding) {
this.disabledBinding.sourceExpression = this.disabledBinding.originalSourceExpression;
this.disabledBinding.originalSourceExpression = null;
this.rebind();
this.disabledBinding = null;
}
}
valueChanged() {
if(this.disabledBinding ) {
if( this.value === true ) {
this.disabledBinding.sourceExpression = this.disabledBinding.originalSourceExpression;
} else {
this.disabledBinding.sourceExpression = this.expression;
}
this.rebind();
} else {
if( this.value === true ) {
this.el.removeAttribute('disabled');
} else {
this.el.setAttribute('disabled', 'disabled');
}
}
}
rebind() {
const source = this.disabledBinding.source;
this.disabledBinding.unbind();
this.disabledBinding.bind(source);
}
}
It is important that the attribute clean up after itself, as I do in the unbind callback. I'll be honest that I'm not sure that the call to rebind is actually necessary in the unbind, but it's there for completeness.