Access vue from imported file - javascript

I'm trying to access my Vue element from a required javascript file but i've failed so far... I want to take element-ui's el-form-item component's rule validator from outside. Maybe my logic is wrong but this way should work too (i guess)
Vue#2.5.13
element-ui#2.0.11
npm#5.6.0
Vue.use(...)
I tried Vue.use(...)
My javascript file.
const MyPlugin = {
install(Vue, options) {
Vue.prototype.usernameValidator = function (rule, value, callback) {
console.log(Vue);
console.log(this);
// ...
My console output:
console.log(Vue);
ƒ Vue$3(options) {
if ("development" !== 'production' &&
!(this instanceof Vue$3)
) {
warn('Vue is a constructor and should be called with the new keyword');
}
this._init(options);
}
console.log(this);
{"required":true,"field":"username","fullField":"username","type":"string"...}
beforeCreate
module.exports = function (rule, value, callback) {
console.log(this)
// ...
});
console.log(this);
{"required":true,"field":"username","fullField":"username","type":"string"...}
As I said my logic may wrong but I'm just curious can I make a method like this ?
Regards
Update
I'm calling it in register.blade.php
<el-form-item label="Username" prop="username" :rules="[{required:true, validator: usernameValidator, trigger:'blur,change'}]">
<el-input v-model="form.username" name="username"></el-input>
</el-form-item>
In app.js;
Vue.use(require('./plugins/usernameValidator'));
// ...
window.app = new Vue({
// ...
});

Where are you requireing the element? I'll detail out how I generally create plugins using your examples.
Define a plugin:
// my-plugin.js
import {FormItem} from 'element-ui'
const MyPlugin = {
install(Vue, options) {
Vue.prototype.usernameValidator = function (rule, value, callback) {
// use FormItem's validator
Then register the plugin via Vue.use:
// app.js
import MyPlugin from 'my-plugin'
// or require
const MyPlugin = require('my-plugin')
Vue.use(MyPlugin)
Later in a component, call the usernameValidator method defined in the plugin:
<template>
<input name="username" v-model="username" :class="isValidUsername ? 'borderGreen' : 'borderRed'" />
<div>{{ usernameFeedback }}</div>
</template>
export default {
beforeCreate () {
// what would you validate in here?
// this.usernameValidator(rule, value, callback)
},
data () {
return {
username: null,
usernameFeedback: null
}
},
computed: {
isValidUsername () {
// assuming the validator returns a boolean, valid/invalid
return this.usernameValidator(rule, this.username, this.validationCallback)
}
},
methods: {
validationCallback (feedback) {
this.usernameFeedback = feedback
}
}
}
If you have an example of how you require a component I can update my answer, but as it is you can access the plugin method in a component using this.

Related

Vue custom directive to simulate getter/setter of computed property

There is an input tag when user type something, I want to add some symbols to the input value. Well, when I need the real value I should remove those symbols from the value. I know I can achieve this using computed property like following:
<template>
<input type="text" v-model="computedTest" /><!-- shows 20$ -->
</template>
<script>
export default {
data() {
return {
test: 20,
}
},
computed: {
computedTest: {
get() {
return this.test + "$"
},
set(val) {
this.test = this.val.replace(/$/g, "")
},
},
methods: {
doSomething() {
console.log(this.test) // 20
},
},
},
}
</script>
Since Vuejs can do this using computed property feature, I believe I can achieve this feature using custom directive. But, I have no idea of the way to do. This is the first try what I did:
<template>
<input type="text" v-model="test" v-custom />
</template>
<script>
export default {
data() {
return {
test: 0
}
},
}
</script>
And the code for v-custom directive:
export default {
bind: function(el, binding, vnode) {
Vue.set(el, "value", toLanguage(el.value, "en")) // toLanguage is a custom function
},
componentUpdated: function(el, binding, vnode) {
// getContext is a custom function and the `contextObj` variable will be equal to
// the whole data property of the component context and the `model` will be equal to
// the v-model expression (in this example, `test`).
let { contextObj, model } = getContext(vnode)
const vnodeValue = get(contextObj, model) // lodash `get` function
// parsing v-model value to remove symbols
const parsedValue = parseInputValue(el, vnodeValue) ?? vnodeValue
if (contextObj) {
Vue.set(contextObj, model, parsedValue)
}
el.value = toLanguage(el.value, "en")
el.dispatchEvent(new Event("input", { bubbles: true }))
},
}
But this directive snippet creates an infinite loop. I'm using Vue2.x. I'll appreciate anybody can help.

How to use an async function as the second parameter to jest.mock?

I need to use jest.mock together with async, await and import() so that I can use a function from another file inside the mocked module. Otherwise I must copy and paste a few hundreds of slocs or over 1000 slocs, or probably it is not even possible.
An example
This does work well:
jest.mock('./myLin.jsx', () => {
return {
abc: 967,
}
});
Everywhere I use abc later it has 967 as its value, which is different than the original one.
This does not work:
jest.mock('./myLin.jsx', async () => {
return {
abc: 967,
}
});
abc seems to not be available.
Actual issue
I need async to be able to do this:
jest.mock('~/config', async () => {
const { blockTagDeserializer } = await import(
'../editor/deserialize' // or 'volto-slate/editor/deserialize'
);
// … here return an object which contains a call to
// blockTagDeserializer declared above; if I can't do this
// I cannot use blockTagDeserializer since it is outside of
// the scope of this function
}
Actual results
I get errors like:
TypeError: Cannot destructure property 'slate' of '((cov_1viq84mfum.s[13]++) , _config.settings)' as it is undefined.
where _config, I think, is the ~/config module object and slate is a property that should be available on _config.settings.
Expected results
No error, blockTagDeserializer works in the mocked module and the unit test is passed.
The unit test code
The code below is a newer not-working code based on this file on GitHub.
import React from 'react';
import renderer from 'react-test-renderer';
import WysiwygWidget from './WysiwygWidget';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-intl-redux';
const mockStore = configureStore();
global.__SERVER__ = true; // eslint-disable-line no-underscore-dangle
global.__CLIENT__ = false; // eslint-disable-line no-underscore-dangle
jest.mock('~/config', async () => {
const { blockTagDeserializer } = await import(
'../editor/deserialize' // or 'volto-slate/editor/deserialize'
);
const createEmptyParagraph = () => {
return {
type: 'p',
children: [{ text: '' }],
};
};
return {
settings: {
supportedLanguages: [],
slate: {
elements: {
default: ({ attributes, children }) => (
<p {...attributes}>{children}</p>
),
strong: ({ children }) => {
return <strong>{children}</strong>;
},
},
leafs: {},
defaultBlockType: 'p',
textblockExtensions: [],
extensions: [
(editor) => {
editor.htmlTagsToSlate = {
STRONG: blockTagDeserializer('strong'),
};
return editor;
},
],
defaultValue: () => {
return [createEmptyParagraph()];
},
},
},
};
});
window.getSelection = () => ({});
test('renders a WysiwygWidget component', () => {
const store = mockStore({
intl: {
locale: 'en',
messages: {},
},
});
const component = renderer.create(
<Provider store={store}>
<WysiwygWidget
id="qwertyu"
title="My Widget"
description="My little description."
required={true}
value={{ data: 'abc <strong>def</strong>' }}
onChange={(id, data) => {
// console.log('changed', data.data);
// setHtml(data.data);
}}
/>
</Provider>,
);
const json = component.toJSON();
expect(json).toMatchSnapshot();
});
What I've tried
The code snippets above show partially what I have tried.
I searched the web for 'jest mock async await import' and did not found something relevant.
The question
If jest.mock is not made to work with async, what else can I do to make my unit test work?
Update 1
In the last snippet of code above, the line
STRONG: blockTagDeserializer('strong'),
uses blockTagDeserializer defined here which uses deserializeChildren, createEmptyParagraph (which is imported from another module), normalizeBlockNodes (which is imported from another module) and jsx (which is imported from another module) functions, which use deserialize which uses isWhitespace which is imported from another module and typeDeserialize which uses jsx and deserializeChildren.
Without using await import(...) syntax how can I fully mock the module so that my unit test works?
If you want to dig into our code, please note that the volto-slate/ prefix in the import statements is for the src/ folder in the repo.
Thank you.
I'd advise not doing any "heavy" stuff (whatever that means) in a callback of jest.mock, – it is designed only for mocking values.
Given your specific example, I'd just put whatever the output of blockTagDeserializer('strong') right inside the config:
jest.mock('~/config', () => {
// ...
extensions: [
(editor) => {
editor.htmlTagsToSlate = {
STRONG: ({ children }) => <strong>{children}</strong>, // or whatever this function actually returns for 'strong'
};
return editor;
},
],
// ...
});
This doesn't require anything asynchronous to be done.
If you need this setup to be present in a lot of files, extracting it in a setup file seems to be the next best thing.
I found a solution. I have a ref callback that sets the htmlTagsToSlate property of the editor in the actual code of the module, conditioned by global.__JEST__ which is defined as true in Jest command line usage:
import { htmlTagsToSlate } from 'volto-slate/editor/config';
[...]
testingEditorRef={(val) => {
ref.current = val;
if (val && global.__JEST__) {
val.htmlTagsToSlate = { ...htmlTagsToSlate };
}
}}
Now the jest.mock call for ~/config is simple, there is no need to do an import in it.
I also use this function:
const handleEditorRef = (editor, ref) => {
if (typeof ref === 'function') {
ref(editor);
} else if (typeof ref === 'object') {
ref.current = editor;
}
return editor;
};

How to test a Vue plugin's functions?

So I have the following class of wrapper functions called cookie.js:
export default {
Vue: null,
set (name, value, options) {
const defaultOptions = {
sameSite: 'lax',
secure: true
};
this.Vue.$cookies.set(name, value, Object.assign(defaultOptions, options));
},
get (name) {
return this.Vue.$cookies.get(name);
},
remove (name) {
this.Vue.$cookies.remove(name);
}
};
And I install this plugin like this:
import cookiePlugin from 'plugins/cookie';
Vue.use({
install (Vue, options) {
Vue.prototype.$cookie = function () {
return Object.assign(cookiePlugin, {
options,
Vue: this
});
};
}
});
I want to create a test class for the set, get, and remove methods in cookie.js.
From looking at various docs and questions on this site, I think the beginning of the setup for the test class is roughly something like:
import Cookie from './cookie';
import { createLocalVue } from 'vue-test-utils'
it('sets up a cookie with correct options', () => {
const localVue = createLocalVue();
localVue.use(Cookie);
// here is where I'm unsure how proceed/call the methods I want to test.
});
I was thinking you do something like localVue.prototype.$cookie().set(...) from here for example, but I don't think thats right since I always get undefined property errors. Other examples seem to show things like localVue.prototype.$plugin where $plugin is a specific method, not like a class of methods like I have.
Any suggestions?

Unable to access Vue.js global function through plugin

I'm trying to refactor some commonly used functions into a globally available Util plugin for my app. I followed the instructions from the docs and this question, but I'm not sure how to use it the functions in the template and Vue keeps complaining about an undefined method. Ideally I just want to call isEmpty from any child component.
util.js
export default {
install(Vue, options) {
Vue.isEmpty = function (object) {
return false // dummy function for now to check if method works
}
}
}
Also tried:
Util.install = function (Vue, options) {
Vue.isEmpty = function () {
...
}
// this doesn't work either
// Vue.prototype.$isEmpty = function (object) {
// return false
// }
}
main.js
import util from './components/shared/util.js'
import comp from './components/shared/myComponent.js'
// Vue.component('util', util) this doesn't work
Vue.use(util)
const app = new Vue({
...
components: {
comp
}).$mount('#app')
None of the below work. The error thrown is TypeError: Cannot read property 'isEmpty' of undefined
component template
<p v-if="util.isEmpty(license)" class="margin-0">N/A</p>
<p v-if="Vue.isEmpty(license)" class="margin-0">N/A</p>
<p v-if="isEmpty(license)" class="margin-0">N/A</p>
You are almost done, are missing of prototype. Try this:
utils.js
export default {
install(Vue, options) {
Vue.prototype.isEmpty = function (object) {
return false // dummy function for now to check if method works
}
}
}
Component
<p v-if="isEmpty(license)" class="margin-0">N/A</p>
Here a example: https://codesandbox.io/s/vue-template-tdx00

How to use enums (or const) in VueJS?

I feel like an idiot for having to ask about something so seemingly simple, but I'm trying to figure out how to use "enums" in VueJS. Currently, in a file called LandingPage.js I have this bit of code:
const Form = {
LOGIN: 0,
SIGN_UP: 1,
FORGOT_PASSWORD: 2,
};
function main() {
new Vue({
el: "#landing-page",
components: {
LoginForm,
WhoIsBehindSection,
WhatIsSection,
Form,
},
data () {
return {
form: Form.LOGIN,
};
},
template: `
<div>
<LoginForm v-if="form === Form.LOGIN"></LoginForm>
<WhatIsSection></WhatIsSection>
<WhoIsBehindSection></WhoIsBehindSection>
</div>
`
});
}
It is the conditional v-if="form === Form.LOGIN" that is failing with the error messages:
Property or method "Form" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
Cannot read property 'LOGIN' of undefined
Just so you guys know without the conditional everything is working, and if I were to put this bit in the template
<p>{{ form }}</p>
it will print 0 on the screen. Though, putting this in the template
<p>{{ Form.LOGIN }}</p>
Will not result in it printing 0 on the screen. So I just cannot for the life of me figure out why it will not accept Form.LOGIN.
The Answer
I did add it to components, but never did I think of adding it to data. Happy that it's working now. :)
data () {
return {
form: Form.LOGIN,
Form, // I had to add this bit
};
},
Thank you MarcRo 👍
If you are using Vue in Typescript, then you can use:
import { TernaryStatus } from '../enum/MyEnums';
export default class MyClass extends Vue {
myVariable: TernaryStatus = TernaryStatus.Started;
TernaryStatus: any = TernaryStatus;
}
and then in Template you can just use
<div>Status: {{ myVariable == TernaryStatus.Started ? "Started It" : "Stopped it" }}</div>
You can use https://stackoverflow.com/a/59714524/3706939.
const State = Object.freeze({ Active: 1, Inactive: 2 });
export default {
data() {
return {
State,
state: State.Active
};
},
methods: {
method() {
return state === State.Active;
}
}
}
You only have access to properties of the Vue instance in your template. Just try accessing window or any global in your template, for example.
Hence, you can access {{ form }} but not {{ Form.LOGIN }}.
A wild guess is that it has something to do with how Vue compiles, but I don't know enough about the internals to answer this.
So just keep declaring all the properties you wish to use in your template in your Vue instance (usually as data).
You can enclose enum into class. All your data, the state, the enum variants would be in one place. The same about behaviours, so you will call form.isLogin() rather than form === Form.LOGIN and form.setLogin() rather than form = Form.Login.
The class to generate enums:
class Fenum {
constructor(start, variants) {
this.state = start;
variants.forEach(value => {
const valueC = value.charAt(0).toUpperCase() + value.slice(1);
this['is' + valueC] = () => this.state === value;
this['set' + valueC] = () => this.state = value;
})
}
}
Example of usage:
function main() {
new Vue({
el: "#landing-page",
components: {
LoginForm,
WhoIsBehindSection,
WhatIsSection,
Form,
},
data () {
return {
form: new Fenum("login", ["login", "signUp", "forgotPassword"]),
};
},
template: `
<div>
<LoginForm v-if="form.isLogin()"></LoginForm>
<WhatIsSection></WhatIsSection>
<WhoIsBehindSection></WhoIsBehindSection>
</div>
`
});
}
Vue observe nested objects, so each call of a set method (from.setLogin(), form.setSignUp(), ...) will trigger updates of the component as it should be.
The generated object from this example:
You can use $options instead of $data https://vuejs.org/v2/api/#vm-options
You can use Proxy to create object which throw runtime errors if someone will read non-defined value or try to add new value - here is createEnum (and use it in data() section)
function createEnum(name,obj) {
return new Proxy(obj, {
get(target, property) {
if (property in target) return target[property];
throw new Error(`ENUM: ${name}.${property} is not defined`);
},
set: (target, fieldName, value) => {
throw new Error(`ENUM: adding new member '${fieldName}' to Enum '${name}' is not allowed.`);
}
});
}
// ---------------
// ----- TEST ----
// ---------------
const Form = createEnum('Form',{
LOGIN: 0,
SIGN_UP: 1,
FORGOT_PASSWORD: 2,
});
// enum value exists
console.log(Form.LOGIN);
// enum value not exists
try{ console.log(Form.LOGOUT) } catch(e){ console.log(e.message)}
// try to add new value
try{ Form.EXIT = 5 } catch(e){ console.log(e.message)}
for string-like Enums where values are equal to keys you can use following helper
export function createEnumArr(name='', values=[]) {
let obj = {};
values.forEach(v => obj[v]=v);
return createEnum(name,obj);
}
const Form = createEnumArr('Form',[
"LOGIN",
"SIGN_UP",
"FORGOT_PASSWORD",
]);
The easiest way!
in main.js
const enumInfo = {
SOURCE_TYPE: {
WALLET: 1,
QR: 2
}
}
Vue.prototype.enumInfo = enumInfo
index.vue
{{enumInfo}}
For 2022 and beyond you should probably be using Vue 3 and Typescript.
The easiest way to use an enum is to map it to string values and then simply return it from your setup function.
<template>
...
<div v-if="mode == DarkModes.DARK">
do something for dark mode
</div>
...
</template>
<script lang="ts">
enum DarkModes {
BRIGHT = 'bright',
DARK = 'dark',
}
export default defineComponent({
name: 'MyDarkOrBrightComponent',
setup() {
const mode = ref(DarkModes.BRIGHT);
...
return {
mode,
DarkModes, // <- PASS YOUR ENUM HERE!
}
}
});
</script>
And if you're using the new <script setup> functionality it's just as easy ... all top level imports are automatically accessible from the template (if you want to put your enum in a separate file).
I've this problem, too.
Here my solution, just put this in the first line:
<script setup>
const Form = {
LOGIN: 0,
SIGN_UP: 1,
FORGOT_PASSWORD: 2,
};
</script>

Categories

Resources