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.
Related
I have this vue component:
export default {
data: function() {
return {
editor: new Editor({
//some options
}),
}
},
computed: {
doc(){ // <--------------------- take attention on this computed property
return this.editor ? this.editor.view.state.doc : null;
},
},
watch: {
doc: {
handler: function(val, OldVal){
// call some this.editor methods
},
deep: true,
immediate: true,
},
},
}
Will computed property doc be dependent on a data property editor if I use this.editor only for checking if it is defined and not use it for assigning it to the doc? I mean, If I will change this.editor will doc be changed? Also, I have watcher on doc so I need to know if I will cause an infinite loop.
In the doc property computation, you use:
the editor property (at the beginning of your ternary, this.editor ? ...)
if editor exists, the editor.view.state.doc property
So the computation of doc will be registered by Vue reactivity system as an effect related to the properties editor and (provided that editor exists) to editor.view.state.doc. In other words, the doc property will be reevaluated each time one of these two properties changes.
=> to reply to the initial question, doc will indeed depend on editor.
This can be toned though, because by 'property change', we mean:
for properties of primitive types, being reassigned with a different value
for objects, having a new reference
So, in our case, if editor, which is an object, is just mutated, and that this mutation does not concern it's property editor.view.state.doc, then doc will not be reevaluated. Here are few examples:
this.editor = { ... } // doc will be reevaluated
this.editor.name = ' ... ' // doc will NOT be reevaluated
this.editor.view.state.doc = { ... } // doc will be reevaluated
If you want to understand this under the hood, I would recommand these resources (for Vue 3):
the reactivity course on Vue Mastery (free)
this great talk and demo (building a simple Vue-like reactivity system)
About the inifinite loop, the doc watcher handler will be executed only:
if doc is reassigned with a different value
in the case where docis an object, if doc is mutated (since you applied the deep option to the doc watcher)
The only possibility to trigger an infinite loop would be to, in the doc watcher handler, mutate or give a new value to doc (or editor.view.state.doc). For example (cf #Darius answer):
watch: {
doc: {
handler: function(val, OldVal){
// we give a new ref each time this handler is executed
// so this will trigger an infinite loop
this.editor.view.state.doc = {}
},
// ...
},
}
=> to reply to the second question, apart from these edge cases, your code won't trigger a loop. For example:
watch: {
doc: {
handler: function(val, OldVal){
// even if we mutate the editor object, this will NOT trigger a loop
this.editor.docsList = []
},
// ...
},
}
Changing editor variable should work, but changing Editor content may not, as it depends on Editor class and how it respects reactivity.
For example:
export default {
data: function() {
return {
editor: {text: '' }
}
}
}
...
this.editor.text = 'Text' // works
this.editor.text = {param: ''} // works
this.editor.text.param = 'value' // works
this.editor.param = {} // does't work, as creation of new property is not observable
If editor observer works and you are changing editor property in observer, which 'reinitializes' internal structures, it may lead to infinite loop:
var Editor = function() {
this.document = {}
this.change = () => { this.document = {} }
}
var data = new Vue({
data: () => ({
editor: new Editor(),
check: 0
}),
watch: {
editor: {
handler() {
this.check++
console.log('Changed')
if (this.check < 5)
this.editor.change()
else
console.log('Infinite loop!')
},
deep: true,
immediate: true
}
}
})
data.editor.change()
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
In such case, extra checking is necessary before making the change.
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;
};
I'm trying to create a directive that has behaviour similar to v-if. Basically if the condition is true, the element should exist, if it's false, it shouldn't. I also need to ensure that those changes pass JEST tests. The purpose is to create a permissions plugin (so I don't need to use v-if="$isAllowed('edit', 'comments')" syntax)
I've looked at this and this tickets. The directive works, as in it replaces the element with a comment in the rendered DOM, however when running tests, the element still exists.
the directive:
Vue.directive('allow', (el, binding, vnode) => {
if (!isAllowed(binding.value)) {
const comment = document.createComment(' ');
Object.defineProperty(comment, 'setAttribute', {
value: () => undefined,
});
vnode.elm = comment;
vnode.text = ' ';
vnode.isComment = true;
vnode.context = undefined;
vnode.tag = undefined;
vnode.data.directives = undefined;
if (vnode.componentInstance) {
vnode.componentInstance.$el = comment;
}
if (el.parentNode) {
el.parentNode.replaceChild(comment, el);
}
}
});
Template:
...
<div v-allow="['edit', 'comments']" test-name="comment">
<!-- some irrelevant code here -->
</div>
...
The test:
it('does not render element when user has no access to it', () => {
expect(wrapper.find('[test-name="comment"]').exists()).toBeFalsy();
});
The test fails (saying component exists). However, when I print the rendered html (console.log(wrapper.html());), it prints comment instead of the component.
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.
I'm aware of click.trigger as well as click.delegate which work fine. But what if I want to assign a click event that should only trigger when the exact element that has the attribute gets clicked?
I'd probably do something like this were it "normal" JS:
el.addEventListener('click', function (e) {
if (e.target === el) {
// continue...
}
else {
// el wasn't clicked directly
}
});
Is there already such an attribute, or do I need to create one myself? And if so, I'd like it to be similar to the others, something like click.target="someMethod()". How can I accomplish this?
Edit: I've tried this which doesn't work because the callback function's this points to the custom attribute class - not the element using the attribute's class;
import { inject } from 'aurelia-framework';
#inject(Element)
export class ClickTargetCustomAttribute {
constructor (element) {
this.element = element;
this.handleClick = e => {
console.log('Handling click...');
if (e.target === this.element && typeof this.value === 'function') {
console.log('Target and el are same and value is function :D');
this.value(e);
}
else {
console.log('Target and el are NOT same :/');
}
};
}
attached () {
this.element.addEventListener('click', this.handleClick);
}
detached () {
this.element.removeEventListener('click', this.handleClick);
}
}
And I'm using it like this:
<div click-target.bind="toggleOpen">
....other stuff...
</div>
(Inside this template's viewModel the toggleOpen() method's this is ClickTargetCustomAttribute when invoked from the custom attribute...)
I'd also prefer if click-target.bind="functionName" could instead be click.target="functionName()" just like the native ones are.
Just use smth like click.delegate="toggleOpen($event)".
$event is triggered event, so you can handle it in toggleOpen
toggleOpen(event) {
// check event.target here
}
Also you can pass any other value available in template context to toggleOpen.