How to identify functional component with vue-eslint-parser - vue.js

I am writing my eslint rule using vue-eslint-parser. It's required to prevent functional components from using vue template syntax.
I have already written a rule for prohibiting the syntax <template functional> :
create(context) {
return context.parserServices.defineDocumentVisitor(
{
VDocumentFragment(node) {
const template = node.children.find(item => item.type === 'VElement' && item.name === 'template');
if (!template) return;
const functionalAttr = template.startTag.attributes.find(item => !item.directive && item.key.name === 'functional');
if (functionalAttr) {
context.report({
message: "Don't use vue templates with functional components",
loc: node.loc
});
}
}
}
);
}
Question:
How do I make the following code also invalid ? (in other words, how to check the script tag and the code inside it).
<template>
</template>
<script>
export default {
functional: true,
};
</script>
Thank you!

Link to the (not very clear) documentation where I found the solution :
create(context: Rule.RuleContext) {
return context.parserServices.defineTemplateBodyVisitor(
{
VElement(node: VElement): void {
if (node.name !== 'template') return;
const functionalAttr = node.startTag.attributes.find(item => !item.directive && item.key.name === 'functional');
if (functionalAttr) {
context.report({
message: "Don't use 'functional' attribute in vue templates. Use render function instead of vue templates for functional components.",
loc: node.loc
});
}
}
},
{
Program(node: ESLintProgram): void {
if (!node.templateBody) return;
const targetNode = node.body.find(item => item.type === 'ExportDefaultDeclaration') as ExportDefaultDeclaration || undefined;
if (!targetNode) return;
const functionalOption = (targetNode.declaration as ObjectExpression).properties.find(item => item.type === 'Property' && (item.key as Identifier).name === 'functional');
if (functionalOption) {
context.report({
message: "Don't use 'functional' property along with vue templates. Use render function instead of vue templates for functional components.",
loc: node.loc
});
}
}
}
);
}

Related

How to implement debounce in vue3

I have a filter input field and want to filter a list of items. The list is large so I want to use debounce to delay the filter being applied until the user has stopped typing for improved user experience. This is my input field and it's bound to filterText that is used to filter the list.
<input type="text" v-model="state.filterText" />
I didn't find any nice solution as I wanted to see my binding in my template so I decided to share my solution. I wrote a simple debounce function and use the following syntax to bind the behavior:
setup() {
...
function createDebounce() {
let timeout = null;
return function (fnc, delayMs) {
clearTimeout(timeout);
timeout = setTimeout(() => {
fnc();
}, delayMs || 500);
};
}
return {
state,
debounce: createDebounce(),
};
},
And the template syntax:
<input
type="text"
:value="state.filterText"
#input="debounce(() => { state.filterText = $event.target.value })"
/>
Hi first time answering something here, so correct my answer as much as you want, I'd appreciate it.
I think that the prettiest and lightest solution is to create a directive globally that you can use as much as you want in all of your forms.
you first create the file with your directive, eg.
debouncer.js
and you create the function for the debouncing
//debouncer.js
/*
This is the typical debouncer function that receives
the "callback" and the time it will wait to emit the event
*/
function debouncer (fn, delay) {
var timeoutID = null
return function () {
clearTimeout(timeoutID)
var args = arguments
var that = this
timeoutID = setTimeout(function () {
fn.apply(that, args)
}, delay)
}
}
/*
this function receives the element where the directive
will be set in and also the value set in it
if the value has changed then it will rebind the event
it has a default timeout of 500 milliseconds
*/
module.exports = function debounce(el, binding) {
if(binding.value !== binding.oldValue) {
el.oninput = debouncer(function(){
el.dispatchEvent(new Event('change'))
}, parseInt(binding.value) || 500)
}
}
After you define this file you can go to your main.js import it and use the exported function.
//main.js
import { createApp } from 'vue'
import debounce from './directives/debounce' // file being imported
const app = createApp(App)
//defining the directive
app.directive('debounce', (el,binding) => debounce(el,binding))
app.mount('#app')
And its done, when you want to use the directive on an input you simply do it like this, no imports or anything.
//Component.vue
<input
:placeholder="filter by name"
v-model.lazy="filter.value" v-debounce="400"
/>
The v-model.lazy directive is important if you choose to do it this way, because by default it will update your binded property on the input event, but setting this will make it wait for a change event instead, which is the event we are emitting in our debounce function. Doing this will stop the v-model updating itself until you stop writing or the timeout runs out (which you can set in the value of the directive).
I hope this was understandable.
<template>
<input type="text" :value="name" #input="test" />
<span>{{ name }}</span>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
function debounce<T> (fn: T, wait: number) {
let timer: ReturnType<typeof setTimeout>
return (event: Event) => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
if (typeof fn === 'function') {
fn(event)
}
}, wait)
}
}
export default defineComponent({
setup () {
const name = ref('test')
function setInputValue (event: Event) {
const target = event.target as HTMLInputElement
name.value = target.value
}
const test = debounce(setInputValue, 1000)
return { name, test }
}
})
</script>
With Lodash, you have an easier solution:
<template>
<input type="text" :value="name" #input="onInput" />
<span>{{ name }}</span>
</template>
<script>
import debounce from "lodash/debounce"
export default {
setup () {
const onInput = debounce(() => {
console.log('debug')
}, 500)
return { onInput }
}
}
</script>
<input #input="updateValue"/>
const updateValue = (event) => {
const timeoutId = window.setTimeout(() => {}, 0);
for (let id = timeoutId; id >= 0; id -= 1) {
window.clearTimeout(id);
}
setTimeout(() => {
console.log(event.target.value)
}, 500);
};
You can try this one
Here's an example with Lodash and script setup syntax using a watcher to fire the debounced api call:
<script setup>
import { ref, watch } from 'vue'
import debounce from 'lodash.debounce'
const searchTerms = ref('')
const getFilteredResults = async () => {
try {
console.log('filter changed')
// make axios call here using searchTerms.value
} catch (err) {
throw new Error(`Problem filtering results: ${err}.`)
}
}
const debouncedFilter = debounce(getFilteredResults, 250) // 250ms delay
watch(() => searchTerms.value, debouncedFilter)
</script>
<template>
<input v-model="searchTerms" />
</template>
https://www.npmjs.com/package/vue-debounce now works for vue 3
It can be registered also with composition API like this
setup() {
...
},
directives: {
debounce: vue3Debounce({ lock: true })
}

Support optional chaining in vuejs

I have created vue and electron app using #vue/cli-service 4.2 in that I am facing a issue of optional chaining.
I can't use ? for validating the condition like (#babel/plugin-proposal-optional-chaining)
eg. a?.b?.c its means it check weather a exist then check for b otherwise
return false same as template expression in angular.
Any one have idea how to configure optional chaining in vuejs.
One quick update is that Vue 3 comes bundled with support for optional chaining.
To test you can try compiling the below Vue component code.
<template>
<div id="app" v-if="user?.username">
#{{ user?.username }} - {{ fullName }} <strong>Followers: </strong>
{{ followers }}
<button style="align-self: center" #click="followUser">Follow</button>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'App',
props: {
test: Object
},
data() {
return {
followers: 0,
user: {
id: 1,
test: {},
username: '_sethAkash',
firstName: undefined,
lastName: 'Seth',
email: 'sethakash007#gmail.com',
isAdmin: true
}
}
},
computed: {
fullName(): string {
//
return `${this?.test?.firstName} ${this?.user?.lastName}`
}
},
methods: {
followUser: function () {
this.followers += 1
}
},
watch: {
followers(newFollowerCount, oldFollowerCount) {
if (oldFollowerCount < newFollowerCount) {
console.log(`${this?.user?.username} has gained a follower!`)
}
}
},
mounted() {
this.followUser()
}
})
</script>
Try vue-template-babel-compiler
It uses Babel to enable Optional Chaining(?.), Nullish Coalescing(??) and many new ES syntax for Vue.js SFC.
Github Repo: vue-template-babel-compiler
DEMO
Usage
1. Install
npm install vue-template-babel-compiler --save-dev
2. Config
1. Vue-CLI
DEMO project for Vue-CLI
// vue.config.js
module.exports = {
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
options.compiler = require('vue-template-babel-compiler')
return options
})
}
}
2. Nuxt.js
DEMO project for Nuxt.js
// nuxt.config.js
export default {
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {
loaders: {
vue: {
compiler: require('vue-template-babel-compiler')
}
},
},
// ...
}
Please refer to REAMDE for detail usage
Support for Vue-CLI, Nuxt.js, Webpack , any environment use vue-loader v15+.
According to this comment on an issue here
You could create a global mixin and use the eval function to evaluate the expression.
Example:
Vue.mixin({
methods: {
$evaluate: param => eval('this.'+param)
}
});
In the template:
<template>
<p>{{ $evaluate('user?.name') }}</p>
</template>
They also added that it might not be perfect:
Although it's still no substitute for the real operator, especially if you have many occurrences of it
Edit
As stated above, using eval may bring some unintended problems, I suggest you use a computed property instead.
In the SFC:
<template>
<p>{{ userName }}</p>
</template>
<script>
export default {
data(){
return {
user: {
firstName: 'Bran'
}
}
},
computed: {
userName(){
return this.user?.firstName
}
}
}
</script>
/*
* Where to use: Use in vue templates to determine deeply nested undefined/null values
* How to use: Instead of writing parent?.child?.child2 you can write
* isAvailable(parent, 'child.child2')
* #author Smit Patel
* #params {Object} parent
* {String} child
* #return {Boolean} True if all the nested properties exist
*/
export default function isAvailable(parent, child) {
try {
const childArray = String(child).split('.');
let evaluted = parent;
childArray.forEach((x) => {
evaluted = evaluted[x];
});
return !!evaluted;
} catch {
return false;
}
}
Use :
<template>
<div>
<span :v-if="isAvailable(data, 'user.group.name')">
{{ data.user.group.name }}
<span/>
</div>
</template>
<script>
import isAvailable from 'file/path';
export default {
methods: { isAvailable }
}
</script>
This doesn't work exactly the same but I think, in this context, it may be event better for most cases.
I used Proxy for the magic method effect. You just need to call the nullsafe method of an object and from there on, just use normal chaining.
In some versions of VueJs you can't specify a default value. It perceives our null safe value as an object (for good reason) and JSON.stringify it, bypassing the toString method. I could override toJSON method but you can't return the string output. It still encodes your return value to JSON. So you end up with your string in quotes.
const isProxy = Symbol("isProxy");
Object.defineProperty(Object.prototype, 'nullsafe', {
enumarable: false,
writable: false,
value: function(defaultValue, maxDepth = 100) {
let treat = function(unsafe, depth = 0) {
if (depth > maxDepth || (unsafe && unsafe.isProxy)) {
return unsafe;
}
let isNullish = unsafe === null || unsafe === undefined;
let isObject = typeof unsafe === "object";
let handler = {
get: function(target, prop) {
if (prop === "valueOf") {
return target[prop];
} else if (typeof prop === "symbol") {
return prop === isProxy ? true : target[prop];
} else {
return treat(target[prop], depth + 1);
}
}
};
let stringify = function() {
return defaultValue || '';
};
let dummy = {
toString: stringify,
includes: function() {
return false;
},
indexOf: function() {
return -1;
},
valueOf: function() {
return unsafe;
}
};
return (isNullish || isObject) ? (new Proxy(unsafe || dummy, handler)) : unsafe;
};
return treat(this);
}
});
new Vue({
el: '#app',
data: {
yoMama: {
a: 1
}.nullsafe('xx'),
yoyoMa: {
b: 1
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
{{ yoMama.yoyoMa.yoMama.yoyoMa.yoMama }}
<hr> {{ yoyoMa.nullsafe('yy').yoMama.yoyoMa.yoMama.yoyoMa }}
</div>
After search many possibilities, I maked one function to help me.
Make one js file to save the helper function and export it
const propCheck = function (obj = {}, properties = ""){
const levels = properties.split(".");
let objProperty = Object.assign({}, obj);
for ( let level of levels){
objProperty = objProperty[level];
if(!objProperty)
return false;
}
return true;
}
export default propCheck;
And install this function for globally in the Vue instance
Vue.prototype.$propCheck = propCheck;
After use in your template
<span>{{$propCheck(person, "name")}}</span>
or
<span>{{$propCheck(person, "contatcs.0.address")}}</span>
or
<span>{{$propCheck(person, "addres.street")}}</span>
Use getSafe() method way for template and js files :)
<template><div>
{{getSafe(() => obj.foo.bar)}} <!-- returns 'baz' -->
{{getSafe(() => obj.foo.doesNotExist)}} <!-- returns undefined -->
</div></template>
<script>
export default {
data() {
return {obj: {foo: {bar: 'baz'}}};
},
methods: {getSafe},
};
function getSafe(fn) {
try { return fn(); }
catch (e) {}
}
</script>
July 2022 Update:
It works with Vue 2.7 (https://blog.vuejs.org/posts/vue-2-7-naruto.html)
2.7 also supports using ESNext syntax in template expressions.
You can use loadash's get method in this case:
_.get(object, path, [defaultValue])
Gets the value at path of object. If the resolved value is undefined, the defaultValue is returned in its place.
https://lodash.com/docs/4.17.15#get
var object = { 'a': [{ 'b': { 'c': 3 } }] };
_.get(object, 'a[0].b.c');
// => 3
_.get(object, 'a.b.c', 'default');
// => 'default'

Vue component computed not reacting

I have 2 components OperatorsList and OperatorButton.
The OperatorsList contains of course my buttons and I simply want, when I click one button, to update some data :
I emit select with the operator.id
This event is captured by OperatorList component, who calls setSelectedOperator in the store
First problem here, in Vue tools, I can see the store updated in real time on Vuex tab, but on the Components tab, the operator computed object is not updated until I click antoher node in the tree : I don't know if it's a display issue in Vue tools or a real data update issue.
However, when it's done, I have another computed property on Vue root element called selectedOperator that should return... the selected operator : its value stays always null, I can't figure out why.
Finally, on the button, I have a v-bind:class that should update when the operator.selected property is true : it never does, even though I can see the property set to true.
I just start using Vue, I'm pretty sure I do something wrong, but what ?
I got the same problems before I used Vuex, using props.
Here is my OperatorList code :
<template>
<div>
<div class="conthdr">Operator</div>
<div>
<operator-button v-for="operator in operators" :op="operator.id"
:key="operator.id" #select="selectOp"></operator-button>
</div>
</div>
</template>
<script>
import OperatorButton from './OperatorButton';
export default {
name: 'operators-list',
components : {
'operator-button': OperatorButton
},
computed : {
operators() { return this.$store.getters.operators },
selected() {
this.operators.forEach(op =>{
if (op.selected) return op;
});
return null;
},
},
methods : {
selectOp(arg) {
this.$store.commit('setSelectedOperator', arg);
}
},
}
</script>
OperatorButton code is
<template>
<span>
<button type="button" v-bind:class="{ sel: operator.selected }"
#click="$emit('select', {'id':operator.id})">
{{ operateur.name }}
</button>
</span>
</template>
<script>
export default {
name: 'operator-button',
props : ['op'],
computed : {
operator() {
return this.$store.getters.operateurById(this.op);
}
},
}
</script>
<style scoped>
.sel{
background-color : yellow;
}
</style>
and finally my app.js look like that :
window.Vue = require('vue');
import Vuex from 'vuex';
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
const store = new Vuex.Store({
state: {
periods : [],
},
mutations: {
setInitialData (state, payload) {
state.periods = payload;
},
setSelectedOperator(state, payload) {
this.getters.operateurs.forEach( op => {
op.selected = (op.id==payload.id)
})
},
},
getters : {
operators : (state) => {
if (Array.isArray(state.periods))
{
let ops = state.periods
.map( item => {
return item.operators
}).flat();
ops.forEach(op => {
// op.selected=false; //replaced after Radu Diță answer by next line :
if (ops.selected === undefined) op.selected=false;
})
return ops;
}
},
operatorById : (state, getters) => (id) => {
return getters.operators.find(operator => operator.id==id);
},
}
});
import Chrono from './components/Chrono.vue';
var app = new Vue({
el: '#app',
store,
components : { Chrono },
mounted () {
this.$store.commit('setInitialData',
JSON.parse(this.$el.attributes.initialdata.value));
},
computed: {
...mapState(['periods']),
...mapGetters(['operators', 'operatorById']),
selectedOperator(){
this.$store.getters.operators.forEach(op =>{
if (op.selected) return op;
});
return null;
}
},
});
Your getter in vuex for operators is always setting selected to false.
operators : (state) => {
if (Array.isArray(state.periods))
{
let ops = state.periods
.map( item => {
return item.operators
}).flat();
ops.forEach(op => {
op.selected=false;
})
return ops;
}
}
I'm guessing you do this for initialisation, but that's a bad place to put it, as you'll never get a selected operator from that getter. Just move it to the proper mutations. setInitialData seems like the right place.
Finally I found where my problems came from :
The $el.attributes.initialdata.value came from an API and the operator objects it contained didn't have a selected property, so I added it after data was set and it was not reactive.
I just added this property on server side before converting to JSON and sending to Vue, removed the code pointed by Radu Diță since it was now useless, and it works.

Launch a Vue modal component outside of a SPA context

We're in the process of retrofitting a mature website with some updated forms. We're replacing some systems entirely with SPAs and some just don't warrant it.
However, we have some system-global modal screens that we need to be available. Some of these have been ported to Vue and work well enough inside SPAs and we'd like to reap the benefits of heightened interactivity in our non-SPA pages.
So whereas before we would have had our event bound like so:
$('a.newBooking').on('click', function(ev){
// code to open bootstrap modal screen, download HTML, etc.
});
How do we spin up a component?
I know I haven't included any serious code here but our modals are basically just embellished versions of the documented example modal component. The difference is we don't have the button. We'll want to be able to launch these modals from all around the page.
My opinion:
For your Modal components:
use singleton pattern for your modal (because basically we only allow one modal popup at the same time), it will make the logic more simple.
customize one install function to add the Vue instances of your Modals to Vue.prototype, like _Vue.prototype.$my = yourModals
then register your plugins in demand like Vue.use(installFunc, {plugins: [SModal, AModal, BModal]})
At your JQuery (or other non-Vue) Apps:
Register Modals to Vue, then create Vue instance
show or hide your modals like vueInstance.$my.SModal.show
Below is one simple demo:
Vue.config.productionTip = false
/*---Modal Plugin---*/
let vm = null // the instance for your Vue modal
let timeout = null //async/delay popup
const SModal = {
isActive: false,
show ({
delay = 500,
message = '',
customClass = 'my-modal-class'
} = {}) {
if (this.isActive) {
vm && vm.$forceUpdate()
return
}
timeout = setTimeout(() => {
timeout = null
const node = document.createElement('div')
document.body.appendChild(node)
let staticClass = ''
vm = new this.__Vue({
name: 's-modal',
el: node,
render (h) { // uses render() which is a closer-to-the-compiler alternative to templates
return h('div', {
staticClass,
'class': customClass,
domProps: {
innerHTML: message
}
})
}
})
}, delay)
this.isActive = true
},
hide () {
if (!this.isActive) {
return
}
if (timeout) {
clearTimeout(timeout)
timeout = null
} else {
vm.$destroy()
document.body.removeChild(vm.$el)
vm = null
}
this.isActive = false
},
__Vue: null,
__installed: false,
install ({ $my, Vue }) {
if (this.__installed) { return }
this.__installed = true
$my.SModal = SModal // added your SModal object to $my
this.__Vue = Vue //get the Vue constructor
}
}
/*---Modal Plugin End---*/
/*---Custom Install Function in order to manage all modals---*/
let installFunc = function (_Vue, opts = {}) {
if (this.__installed) {
return
}
this.__installed = true
const $my = {
'memo': 'I am a plugin management.'
}
if (opts.plugins) {
Object.keys(opts.plugins).forEach(key => {
const p = opts.plugins[key]
if (typeof p.install === 'function') {
p.install({ $my, Vue: _Vue })
}
})
}
_Vue.prototype.$my = $my
}
/*---Install Plugins---*/
Vue.use(installFunc, {
plugins: [SModal]
})
let globalVue = new Vue({
el: '#vue-app'
})
$('#test').on('click', 'span', function () {
globalVue.$my.SModal.isActive ? globalVue.$my.SModal.hide() : globalVue.$my.SModal.show({'message':'test', 'delay':100})
})
span {
cursor:pointer;
color:red;
}
.my-modal-class {
position:absolute;
top:50px;
left:150px;
width:200px;
height:200px;
background-color:red;
z-index:9999;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="vue-app">
</div>
<div id="test">
<h3>One Example</h3>
<p><span>Hello</span>, how are you?</p>
<p>Me? I'm <span>good</span>.</p>
</div>

User-switchable custom themes with Vue.js

I have a VueJS app that will come with many different themes (at least 20 or so). Each theme stylesheet not only changes things like color and font size, but also the position and layout of some elements as well.
I want the user to be able to switch between these themes dynamically. So, at runtime, the user will be able to open an Options menu and select from a dropdown.
What is the cleanest way to have many dynamic user-selectable themes in VueJS?
I've thought of a couple of ways, such as:
Dynamically inserting a <link> or <style> tag. While this might work, I don't really see it as particularly "clean", and if I'm loading from AJAX, then oftentimes I'll see a FOUC.
Simply changing the Vue class bindings through a computed property. Something like having a if-else chain for every supported theme in every component. I don't particularly like this solution, as then every component I make will need to be updated every time I add a new theme later on.
In React, I think there's a plugin or something that has a <ThemeProvider> component, where adding a theme is as simple as wrapping it, i.e. <ThemeProvider theme={themeProp}><MyComponent></ThemeProvider>, and all styles in that theme will apply to that component and all child components.
Does VueJS have something similar, or is there a way to implement it?
I will admit I had some fun with this one. This solution does not depend on Vue, but it can easily by used by Vue. Here we go!
My goal is to create a "particularly clean" dynamic insertion of <link> stylesheets which should not result in a FOUC.
I created a class (technically, it's a constructor function, but you know what I mean) called ThemeHelper, which works like this:
myThemeHelper.add(themeName, href) will preload a stylesheet from href (a URL) with stylesheet.disabled = true, and give it a name (just for keeping track of it). This returns a Promise that resolves to a CSSStyleSheet when the stylesheet's onload is called.
myThemeHelper.theme = "<theme name>"(setter) select a theme to apply. The previous theme is disabled, and the given theme is enabled. The switch happens quickly because the stylesheet has already been pre-loaded by .add.
myThemeHelper.theme (getter) returns the current theme name.
The class itself is 33 lines. I made a snippet that switches between some Bootswatch themes, since those CSS files are pretty large (100Kb+).
const ThemeHelper = function() {
const preloadTheme = (href) => {
let link = document.createElement('link');
link.rel = "stylesheet";
link.href = href;
document.head.appendChild(link);
return new Promise((resolve, reject) => {
link.onload = e => {
const sheet = e.target.sheet;
sheet.disabled = true;
resolve(sheet);
};
link.onerror = reject;
});
};
const selectTheme = (themes, name) => {
if (name && !themes[name]) {
throw new Error(`"${name}" has not been defined as a theme.`);
}
Object.keys(themes).forEach(n => themes[n].disabled = (n !== name));
}
let themes = {};
return {
add(name, href) { return preloadTheme(href).then(s => themes[name] = s) },
set theme(name) { selectTheme(themes, name) },
get theme() { return Object.keys(themes).find(n => !themes[n].disabled) }
};
};
const themes = {
flatly: "https://bootswatch.com/4/flatly/bootstrap.min.css",
materia: "https://bootswatch.com/4/materia/bootstrap.min.css",
solar: "https://bootswatch.com/4/solar/bootstrap.min.css"
};
const themeHelper = new ThemeHelper();
let added = Object.keys(themes).map(n => themeHelper.add(n, themes[n]));
Promise.all(added).then(sheets => {
console.log(`${sheets.length} themes loaded`);
themeHelper.theme = "materia";
});
<h3>Click a button to select a theme</h3>
<button
class="btn btn-primary"
onclick="themeHelper.theme='materia'">Paper theme
</button>
<button
class="btn btn-primary"
onclick="themeHelper.theme='flatly'">Flatly theme
</button>
<button
class="btn btn-primary"
onclick="themeHelper.theme='solar'">Solar theme
</button>
It is not hard to tell that I'm all about ES6 (and maybe I overused const just a bit :)
As far as Vue goes, you could make a component that wraps a <select>:
const ThemeHelper = function() {
const preloadTheme = (href) => {
let link = document.createElement('link');
link.rel = "stylesheet";
link.href = href;
document.head.appendChild(link);
return new Promise((resolve, reject) => {
link.onload = e => {
const sheet = e.target.sheet;
sheet.disabled = true;
resolve(sheet);
};
link.onerror = reject;
});
};
const selectTheme = (themes, name) => {
if (name && !themes[name]) {
throw new Error(`"${name}" has not been defined as a theme.`);
}
Object.keys(themes).forEach(n => themes[n].disabled = (n !== name));
}
let themes = {};
return {
add(name, href) { return preloadTheme(href).then(s => themes[name] = s) },
set theme(name) { selectTheme(themes, name) },
get theme() { return Object.keys(themes).find(n => !themes[n].disabled) }
};
};
let app = new Vue({
el: '#app',
data() {
return {
themes: {
flatly: "https://bootswatch.com/4/flatly/bootstrap.min.css",
materia: "https://bootswatch.com/4/materia/bootstrap.min.css",
solar: "https://bootswatch.com/4/solar/bootstrap.min.css"
},
themeHelper: new ThemeHelper(),
loading: true,
}
},
created() {
// add/load themes
let added = Object.keys(this.themes).map(name => {
return this.themeHelper.add(name, this.themes[name]);
});
Promise.all(added).then(sheets => {
console.log(`${sheets.length} themes loaded`);
this.loading = false;
this.themeHelper.theme = "flatly";
});
}
});
<script src="https://unpkg.com/vue#2.5.2/dist/vue.js"></script>
<div id="app">
<p v-if="loading">loading...</p>
<select v-model="themeHelper.theme">
<option v-for="(href, name) of themes" v-bind:value="name">
{{ name }}
</option>
</select>
<span>Selected: {{ themeHelper.theme }}</span>
</div>
<hr>
<h3>Select a theme above</h3>
<button class="btn btn-primary">A Button</button>
I hope this is as useful to you as it was fun for me!
Today I found possibly the simplest way to solve this and it even works with SCSS (no need to have separate CSS for each theme, which is important if your themes are based on one library and you only want to define the changes), but it needs
Make an .scss/.css file for each theme
Make these available somewhere in the src folder, src/bootstrap-themes/dark.scss for example
Import the .scss with a condition in the App.vue, in the created:, for example
if (Vue.$cookies.get('darkmode') === 'true') {
import('../bootstrap-themes/dark.scss');
this.nightmode = true;
} else {
import('../bootstrap-themes/light.scss');
this.nightmode = false;
}
When the user lands on the page, I read the cookies and see if they left nightmode enabled when they left last time and load the correct scss
When they use the switch to change the theme, this method is called, which saves the cookie and reloads the page, which will then read the cookie and load the correct scss
setTheme(nightmode) {
this.$cookies.set("darkmode", nightmode, "7d")
this.$router.go()
}
One very simple and working approach: Just change the css class of your body dynamically.
how about this,
https://www.mynotepaper.com/create-multiple-themes-in-vuejs
and this,
https://vuedose.tips/tips/theming-using-custom-properties-in-vuejs-components/
I think that will give you a basic idea for your project.
first of all I would like to thank ContinuousLoad for its inspiring code snippet.
It helped me a lot to make my own theme chooser.
I just wanted to give some feedback and share my changes to original code, specially in function preloadTheme. The biggest change was to remove the onload() event listener after initial load, because it would re-run each time you change the link.disabled value, at least under Firefox.
Hope it helps :)
const ThemeHelper = function() {
const preloadTheme = href => {
let link = document.createElement('link');
link.rel = 'stylesheet';
link.disabled = false;
link.href = href;
return new Promise((resolve, reject) => {
link.onload = function() {
// Remove the onload() event listener after initial load, because some
// browsers (like Firefox) could call onload() later again when changing
// the link.disabled value.
link.onload = null;
link.disabled = true;
resolve(link);
};
link.onerror = event => {
link.onerror = null;
reject(event);
};
document.head.appendChild(link);
});
};
const selectTheme = (themes, name) => {
if (name && !themes[name]) {
throw new Error(`"${name}" has not been defined as a theme.`);
}
Object.keys(themes).forEach(n => {
if (n !== name && !themes[n].disabled) themes[n].disabled = true;
});
if (themes[name].disabled) themes[name].disabled = false;
};
let themes = {};
return {
add(name, href) {
return preloadTheme(href).then(s => (themes[name] = s));
},
set theme(name) {
selectTheme(themes, name);
},
get theme() {
return Object.keys(themes).find(n => !themes[n].disabled);
}
};
};
let app = new Vue({
el: '#app',
data() {
return {
themes: {
flatly: 'https://bootswatch.com/4/flatly/bootstrap.min.css',
materia: 'https://bootswatch.com/4/materia/bootstrap.min.css',
solar: 'https://bootswatch.com/4/solar/bootstrap.min.css'
},
themeHelper: new ThemeHelper(),
loading: true
};
},
created() {
// add/load themes
let added = Object.keys(this.themes).map(name => {
return this.themeHelper.add(name, this.themes[name]);
});
Promise.all(added).then(sheets => {
console.log(`${sheets.length} themes loaded`);
this.loading = false;
this.themeHelper.theme = 'flatly';
});
}
});
<script src="https://unpkg.com/vue#2.5.2/dist/vue.js"></script>
<div id="app">
<p v-if="loading">loading...</p>
<select v-model="themeHelper.theme">
<option v-for="(href, name) of themes" v-bind:value="name">
{{ name }}
</option>
</select>
<span>Selected: {{ themeHelper.theme }}</span>
</div>
<hr>
<h3>Select a theme above</h3>
<button class="btn btn-primary">A Button</button>