Vue.js bind reactive style object px params - vue.js

I'm try do reactive change style HTML element from range input, but there is a problem style params font-size, height, width and more with px
<span v-bind:style="myobj.css">Hi all</span>
var vm = new Vue({
el: '#app',
data: {
myobj: {
css: {
color: '#999999',
fontSize: '18',
}
}
}
})
<input v-model="myobj.css.fontSize" type="range" min="10" max="32" step="1">
but needly - "font-size: 18px, how i can do it? I'm try use filter, but is doesnt work with obj

You could use a computed property for your v-model:
computed: {
fontSize: {
get() {
const fontPx = /(<?value>\d+(?:\.\d+)?)px/
if (!fontPx.test(newValue))
return 0
const { groups: { value } } = newValue.match(fontPx)
return value
},
set(newValue) {
this.myobj.css.fontsize = newValue + 'px'
}
}
}
and in your template:
<input v-model="fontSize" type="range" min="10" max="32" step="1">
EDIT - I just re-read your question. I had the get and set flipped around. The edited answer should do what you need.
Alternatively, you could use a computed property for the style instead:
computed: {
style() {
const style = { ...this.myobj.css }
style.fontsize = style.fontsize + 'px'
return style
}
}
<span v-bind:style="style">Hi all</span>

Related

How to change text in sidebar dynamically based on component I click on?

I am trying to make a timeline website. I would like to click on a Interval component (such as Russo-Persian War in the code below) and a text-description to show on the sidebar. There is currently no text description set. The sidebar is currently set to "Lorem Ipsum". How can I dynamically change the text in the sidebar based on the Interval component I click on? Thank you.
Here is my code:
Sidebar.vue
<div id="side-bar">
<hr />
{{text}}
</div>
</template>
<script>
import Vue from 'vue'
export default Vue.extend({
data() {
return {
text: `Lorem Ipsum`,
}
},
methods: {
},
})
</script>
Helper.js
export class Mark {
name = ''
tags = []
constructor(date) {
this.year = date
}
}
/* An interval consist of two marks (dates) in time-history */
export class Interval {
from = null
to = null
subIntervals = null
name = ''
i18n = null
tags = []
constructor() {
if (arguments.length === 1) {
let data = arguments[0]
this.from = new Mark(data.from)
this.to = new Mark(data.to)
if (data.subIntervals !== undefined) {
this.subIntervals = data.subIntervals
}
this.i18n = {
messages: {
es: { message: { title: data.title } }
}
}
} else {
this.from = arguments[0]
this.to = arguments[1]
}
}
}
/* A 'Timeline' consist of a 'name' and a
* property 'events', which consists of an ordered list
* of 'Interval's AND/OR 'Mark's (or even other 'Timeline's) */
export class Timeline {
events = []
name = ''
tags = []
constructor(_name) {
this.name = _name
}
}
Data.js
import { Interval, Timeline, century } from './Helper'
...
new Interval({
title: '1800s',
from: 1800,
to: 1899,
})
let lower = new Timeline()
let ninteenthcentury = new Timeline('1900s')
ninteenthcentury.events.push(
new Interval({
title: 'Russo-Persian War',
from: 1804,
to: 1813,
}),
new Interval({
title: 'Russo-Turkish War',
from: 1806,
to: 1812,
})
(etc...)
)
lower.events.push(
ninteenthcentury
)
export { lower }
Format.vue
<template>
<div class="interval"
:style="{
width: width + 'px',
marginLeft: marginLeft + 'px',
display: width < 51 ? 'none' : 'flex'
}"
:title="title">
<span v-on:click= "" class="name" >{{$t('message.title')}}</span>
<!-- sub intervals -->
<div class="lane" v-if="data.subIntervals">
<interval
v-for="(subInterval, index) in data.subIntervals"
:key="index"
:data="subInterval"
:left="last(index)"
:ratio="ratio" />
</div>
</div>
</template>
<script>
import Vue from 'vue'
export default Vue.extend({
name: 'interval',
props: ['data', 'ratio', 'left'],
i18n: {
},
created: function() {
if (this.data.i18n) {
this.$i18n.setLocaleMessage('es', this.data.i18n.messages.es)
}
},
methods: {
last(index) {
if (index > 0) {
return this.data.subIntervals[index - 1].to.year
}
return this.data.from.year
},
},
computed: {
marginLeft() {
if (this.left) {
return Math.abs(this.left - this.data.from.year) * this.ratio
}
return 0
},
width() {
return (this.data.to.year - this.data.from.year) * this.ratio
},
title() {
return this.$i18n.t('message.title') + ` (${this.data.from.year},${this.data.to.year})`
},
}
})
</script>
First of all you should consider migrating your helper to vuex, using store features will help you in every thing.
You will have all intervals in one array and, for instance, have a prop in your store for currentInterval that changes every time you click an interval on the timeline. You can achieve this by dispatching a vuex action to the store that will mutate the state of your prop, ex currentInterval.
In the component where you show the current interval data you will ...mapGetters from the store that will always return that prop currentInterval of the store.
More information about store management here: enter link description here

VUE Js child is not updating when parent updates

I am using VUE JS and I want to have group of checkboxes as below. When someone clicked on main checkbox all the checkboxes under that check box should be selected. Please find the attached image for your reference
To accomplish this scenario I am using 2 main components. When someone clicked on a component I am adding that component to selectedStreams array. Selected stream array structure is similar to below structure
checked: {
g1: ['val1'],
g2: []
}
When I click on heading checkbox I am triggering function
clickAll and try to change the selectedStreams[keyv].
But this action doesn't trigger the child component and automatically checked the checkbox.
Can I know the reason why when I changed the parent v-model value it is not visible in child UI.
Parent Component
<template>
<div>
<b-form-group>
<StreamCheckBox
v-for="(opts, keyv) in loggedInUsernamesf"
:key="keyv"
:name="opts"
:main="keyv"
v-model="selectedStreams[keyv]"
#clickAll="clickAll($event, keyv)"
></StreamCheckBox>
</b-form-group>
</div>
</template>
<script>import StreamCheckBox from "./StreamCheckBox";
export default {
name: "GroupCheckBox",
components: {StreamCheckBox},
data(){
return {
selectedStreams:{}
}
},
computed: {
loggedInUsernamesf() {
var username_arr = JSON.parse(this.$sessionStorage.access_info_arr);
var usernames = {};
if (!username_arr) return;
for (let i = 0; i < username_arr.length; i++) {
usernames[username_arr[i].key] = [];
var payload = {};
payload["main"] = username_arr[i].val.name;
payload["type"] = username_arr[i].val.type;
if (username_arr[i].val.permissions) {
for (let j = 0; j < username_arr[i].val.permissions.length; j++) {
payload["value"] = username_arr[i].key + username_arr[i].val.permissions[j].streamId;
payload["text"] = username_arr[i].val.permissions[j].streamId;
}
}
usernames[username_arr[i].key].push(payload);
}
return usernames;
},
},
methods: {
clickAll(e, keyv) {
if (e && e.includes(keyv)) {
this.selectedStreams[keyv] = this.loggedInUsernamesf[keyv].map(
opt => {
return opt.value
}
);
}
console.log(this.selectedStreams[keyv]);
}
}
}
</script>
<style scoped>
</style>
Child Component
<template>
<div style="text-align: left;">
<b-form-checkbox-group style="padding-left: 0;"
id="flavors"
class="ml-4"
stacked
v-model="role"
:main="main"
>
<b-form-checkbox
class="font-weight-bold main"
:main="main"
:value="main"
#input="checkAll(main)"
>{{ name[0].main }}</b-form-checkbox>
<b-form-checkbox
v-for="opt in displayStreams"
:key="opt.value"
:value="opt.value"
>{{ opt.text }}</b-form-checkbox>
</b-form-checkbox-group>
</div>
</template>
<script>
export default {
name:"StreamCheckBox",
props: {
value: {
type: Array,
},
name: {
type: Array,
},
main:{
type:String
}
},
computed:{
role: {
get: function(){
return this.value;
},
set: function(val) {
this.$emit('input', val);
}
},
displayStreams: function () {
return this.name.filter(i => i.value)
},
},
methods:{
checkAll(val)
{
this.$emit('clickAll', val);
}
}
}
</script>
First of all, I am not sure what is the purpose of having props in your parent component. I think you could just remove props from your parent and leave in the child component only.
Second of all, the reason for not triggering the changes could be that you are dealing with arrays, for that you could set a deep watcher in your child component:
export default {
props: {
value: {
type: Array,
required: true,
},
},
watch: {
value: {
deep: true,
//handle the change
handler() {
console.log('array updated');
}
}
}
}
You can find more info here and here.

vue: changes not triggered #input

Below is vue script - the concern method is called notLegalToShip which checks when age < 3.
export default {
template,
props: ['child', 'l'],
created() {
this.name = this.child.name.slice();
this.date_of_birth = this.child.date_of_birth.slice();
},
data() {
return {
edit: false,
today: moment().format('DD/MM/YYYY'),
childUnder3: false
};
},
computed: {
age() {
var today = new Date();
var birthDate = new Date(this.child.date_of_birth);
var age = today.getFullYear() - birthDate.getFullYear();
var m = today.getMonth() - birthDate.getMonth();
if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
}
},
methods: Object.assign(
mapActions(['updateChild']),
{
notLegalToShip() {
if(this.age < 3){
this.childUnder3 = true;
}
this.childUnder3 = false;
},
showForm() {
this.edit = true;
},
hideForm() {
this.edit = false;
},
submitForm() {
this.hideForm();
this.updateChild({
child: this.child,
name: this.name,
dateOfBirth: this.date_of_birth,
childUnder3 : this.childUnder3
});
}
}
)
}
Here's the snippet of my template. The input as below.
I want the notLegalToShip method to be triggered when I click arrow changing the year. A warning will appear when childUnder3 is "true". I've tried #change, #input on my input but my method is not triggered at all:
<div>
{{childUnder3}}
{{age}}
<div class="callout danger" v-if="childUnder3">
<h2>Sorry</h2>
<p>Child is under 3!</p>
</div>
<div v-if="!edit">
<a #click.prevent="showForm()" href="#" class="more-link edit-details edit-child">
<i class="fa fa-pencil" aria-hidden="true"></i>{{ l.child.edit_details }}
</a>
</div>
<form v-show="edit" #submit.prevent="submitForm()">
<div class="input-wrap">
<label for="account__child__date-of-birth__date">{{ l.child.date_of_birth }}</label>
<input id="account__child__date-of-birth__date" type="date" name="date_of_birth" v-on:input="notLegalToShip" v-model="date_of_birth" v-validate="'required'">
<p class="error-message" v-show="errors.has('date_of_birth')">{{ l.child.date_of_birth_invalid }}</p>
</div>
</form>
</div>
Any help checking my code above would be appreciated!
You have a couple of problems...
Initialise the name and date_of_birth properties in the data() initialiser so Vue can react to them. You can even initialise them from your child prop there...
data() {
return {
edit: false,
today: moment().format('DD/MM/YYYY'),
name: this.child.name // no need to use slice, strings are immutable
date_of_birth: this.child.date_of_birth
}
}
Use this.date_of_birth inside your age computed property instead of this.child.date_of_birth. This way, it will react to changes made via your v-model="date_of_birth" input element.
Make childUnder3 a computed property, it will be easier that way
childUnder3() {
return this.age < 3
}
Alternately, ditch this and just use v-if="age < 3"
With the above, you no longer need any #input or #change event listeners.

Custom vue directive to render only if present

Frequently, I want to render a div (or other element) only if it has content. This means repeating the reference to the content in the tag, and in v-if, like this...
<div v-if="store.sometimesIWillBeEmpty">{{store.sometimesIWillBeEmpty}}</div>
With custom directives, I want to create a directive, v-fill, that behaves just like the code above, but with simplified syntax...
<div v-fill="store.sometimesIWillBeEmpty"></div>
updated The following works when message is not empty. What do I set or clear to render nothing when message is empty?
var store = {message: "hello cobber"}
Vue.directive('fill',
function (el, binding, vnode) {
if(binding.value)
el.innerHTML = binding.value
else
el = null
}
);
new Vue({
el: '#fill-example',
data: {
store: store
}
})
I'm one line away. Here's my fiddle. Anyone have any ideas?
It is possible to make a straightforward component to do what you want. A directive requires a bit more manipulation to be able to remove the element and put it back in the right place.
const vm = new Vue({
el: '#fill-example',
data: {
empty: '',
notEmpty: 'I have content'
},
components: {
renderMaybe: {
props: ['value'],
template: `<div v-if="value" class="boxy">{{value}}</div>`
}
},
directives: {
fill: {
bind(el, binding) {
Vue.nextTick(() => {
el.vFillMarkerNode = document.createComment('');
el.parentNode.insertBefore(el.vFillMarkerNode, el.nextSibling);
if (binding.value) {
el.textContent = binding.value;
} else {
el.parentNode.removeChild(el);
}
});
},
update(el, binding) {
if (binding.value) {
el.vFillMarkerNode.parentNode.insertBefore(el, el.vFillMarkerNode);
el.textContent = binding.value;
} else {
if (el.parentNode) {
el.parentNode.removeChild(el);
}
}
}
}
}
});
setTimeout(() => {
vm.empty = "Now I have content, too.";
}, 1500);
.boxy {
border: thin solid black;
padding: 1em;
}
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<div id="fill-example">
Components:
<render-maybe :value="empty"></render-maybe>
<render-maybe :value="notEmpty"></render-maybe>
Directives:
<div class="boxy" v-fill="empty"></div>
<div class="boxy" v-fill="notEmpty"></div>
</div>

Vue : Limit characters in text area input, truncate filter?

<textarea name="" id="" cols="30" rows="10" v-model="$store.state.user.giftMessage | truncate 150"></textarea>
I tried creating a custom filter :
filters: {
truncate(text, stop, clamp) {
return text.slice(0, stop) + (stop < text.length ? clamp || '...' : '')
}
}
but that didn't broke the build when I put it on the v-model for the input...
Any advice?
This is one of those cases, where you really want to use a component.
Here is an example component that renders a textarea and limits the amount of text.
Please note: this is not a production ready, handle all the corner cases component. It is intended as an example.
Vue.component("limited-textarea", {
props:{
value:{ type: String, default: ""},
max:{type: Number, default: 250}
},
template: `
<textarea v-model="internalValue" #keydown="onKeyDown"></textarea>
`,
computed:{
internalValue: {
get() {return this.value},
set(v){ this.$emit("input", v)}
}
},
methods:{
onKeyDown(evt){
if (this.value.length >= this.max) {
if (evt.keyCode >= 48 && evt.keyCode <= 90) {
evt.preventDefault()
return
}
}
}
}
})
This component implements v-model and only emits a change to the data if the length of the text is less than the specified max. It does this by listening to keydown and preventing the default action (typing a character) if the length of the text is equal to or more than the allowed max.
console.clear()
Vue.component("limited-textarea", {
props:{
value:{ type: String, default: ""},
max:{type: Number, default: 250}
},
template: `
<textarea v-model="internalValue" #keydown="onKeyDown"></textarea>
`,
computed:{
internalValue: {
get() {return this.value},
set(v){ this.$emit("input", v)}
}
},
methods:{
onKeyDown(evt){
if (this.value.length >= this.max) {
if (evt.keyCode >= 48 && evt.keyCode <= 90) {
evt.preventDefault()
return
}
}
}
}
})
new Vue({
el: "#app",
data:{
text: ""
}
})
<script src="https://unpkg.com/vue#2.4.2"></script>
<div id="app">
<limited-textarea v-model="text"
:max="10"
cols="30"
rows="10">
</limited-textarea>
</div>
Another issue with the code in the question is Vuex will not allow you set a state value directly; you have to do it through a mutation. That said, there should be a Vuex mutation that accepts the new value and sets it, and the code should commit the mutation.
mutations: {
setGiftMessage(state, message) {
state.user.giftMessage = message
}
}
And in your Vue:
computed:{
giftMessage:{
get(){return this.$store.state.user.giftMessage},
set(v) {this.$store.commit("setGiftMessage", v)}
}
}
Technically the code should be using a getter to get the user (and it's giftMessage), but this should work. In the template you would use:
<limited-textarea cols="30" rows="10" v-model="giftMessage"></limited-textarea>
Here is a complete example using Vuex.
console.clear()
const store = new Vuex.Store({
state:{
user:{
giftMessage: "test"
}
},
getters:{
giftMessage(state){
return state.user.giftMessage
}
},
mutations:{
setGiftMessage(state, message){
state.user.giftMessage = message
}
}
})
Vue.component("limited-textarea", {
props:{
value:{ type: String, default: ""},
max:{type: Number, default: 250}
},
template: `
<textarea v-model="internalValue" #keydown="onKeyDown"></textarea>
`,
computed:{
internalValue: {
get() {return this.value},
set(v){ this.$emit("input", v)}
}
},
methods:{
onKeyDown(evt){
if (this.value.length >= this.max) {
if (evt.keyCode >= 48 && evt.keyCode <= 90) {
evt.preventDefault()
return
}
}
}
}
})
new Vue({
el: "#app",
store,
computed:{
giftMessage:{
get(){ return this.$store.getters.giftMessage},
set(v){ this.$store.commit("setGiftMessage", v)}
}
}
})
<script src="https://unpkg.com/vue#2.4.2"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/2.4.0/vuex.js"></script>
<div id="app">
<limited-textarea v-model="giftMessage"
:max="10"
cols="30"
rows="10">
</limited-textarea>
Message: {{giftMessage}}
</div>
Sorry to break in. Was looking for a solution. Looked at all of them.
For me they look too complicated. I'm always looking for symplicity.
Therefor I like the answer of #Даниил Пронин. But it has the by #J. Rambo noted potential problem.
To stay as close as possible to the native html textelement. The solution I came up with is:
Vue Template
<textarea v-model="value" #input="assertMaxChars()">
JavaScript
let app = new Vue({
el: '#app',
data: {
value: 'Vue is working!',
maxLengthInCars: 25
},
methods: {
assertMaxChars: function () {
if (this.value.length >= this.maxLengthInCars) {
this.value = this.value.substring(0,this.maxLengthInCars);
}
}
}
})
Here is a REPL link: https://repl.it/#tsboh/LimitedCharsInTextarea
The upside I see is:
the element is as close as possible to the native element
simple code
textarea keeps focus
delete still works
works with pasting text as well
Anyway
happy coding
While I agree with the selected answer. You can also easily prevent the length using a keydown event handler.
Vue Template
<input type="text" #keydown="limit( $event, 'myModel', 3)" v-model="myModel" />
JavaScript
export default {
name: 'SomeComponent',
data () {
return {
myModel: ''
};
},
methods: {
limit( event, dataProp, limit ) {
if ( this[dataProp].length >= limit ) {
event.preventDefault();
}
}
}
}
Doing this way, you can also use regular expression to event prevent the type of keys accepted. For instance, if you only wanted to accept numeric values you can do the following.
methods: {
numeric( event, dataProp, limit ) {
if ( !/[0-9]/.test( event.key ) ) {
event.preventDefault();
}
}
}
I have improved on #J Ws answer. The resulting code does not have to define how to react on which keypress, which is why it can be used with any character in contrast to the accepted answer. It only cares about the string-length of the result. It also can handle Copy-Paste-actions and cuts overlong pastes to size:
Vue.component("limitedTextarea", {
props: {
value: {
type: String,
default: ""
},
max: {
type: Number,
default: 25
}
},
computed: {
internalValue: {
get: function () {
return this.value;
},
set: function (aModifiedValue) {
this.$emit("input", aModifiedValue.substring(0, this.max));
}
}
},
template: '<textarea v-model="internalValue" #keydown="$forceUpdate()" #paste="$forceUpdate()"></textarea>'
});
The magic lies in the #keydown and #paste-events, which force an update. As the value is already cut to size correctly, it assures that the internalValue is acting accordingly.
If you also want to protect the value from unchecked script-changes, you can add the following watcher:
watch: {
value: function(aOldValue){
if(this.value.length > this.max){
this.$emit("input", this.value.substring(0, this.max));
}
}
}
I just found a problem with this easy solution: If you set the cursor somewhere in the middle and type, transgressing the maximum, the last character is removed and the cursor set to the end of the text. So there is still some room for improvement...
My custom directive version. Simple to use.
<textarea v-model="input.textarea" v-max-length="10"></textarea>
Vue.directive('maxlength',{
bind: function(el, binding, vnode) {
el.dataset.maxLength = Number(binding.value);
var handler = function(e) {
if (e.target.value.length > el.dataset.maxLength) {
e.target.value = e.target.value.substring(0, el.dataset.maxLength);
var event = new Event('input', {
'bubbles': true,
'cancelable': true
});
this.dispatchEvent(event);
return;
}
};
el.addEventListener('input', handler);
},
update: function(el, binding, vnode) {
el.dataset.maxLength = Number(binding.value);
}
})
Event() has browser compatibility issue.
Unfortunately for me, keydown approach seems not working good with CJK.
there can be side effects since this method fires input event double time.
Best way is to use watch to string length and set old value if string is longer than you want:
watch: {
'inputModel': function(val, oldVal) {
if (val.length > 250) {
this.inputModel = oldVal
}
},
},
Simply use maxlength attribute like this:
<textarea v-model="value" maxlength="50" />
I used your code and broke it out into a .Vue component, thanks!
<template>
<textarea v-model="internalValue" #keydown="onKeyDown"></textarea>
</template>
<script>
export default {
props:{
value:{ type: String, default: ""},
max:{type: Number, default: 250}
},
computed:{
internalValue: {
get() {return this.value},
set(v){ this.$emit("input", v)}
}
},
methods:{
onKeyDown(evt){
if (this.value.length >= this.max) {
evt.preventDefault();
console.log('keydown');
return
}
}
}
}
I did this using tailwind 1.9.6 and vue 2:
<div class="relative py-4">
<textarea
v-model="comment"
class="w-full border border-gray-400 h-16 bg-gray-300 p-2 rounded-lg text-xs"
placeholder="Write a comment"
:maxlength="100"
/>
<span class="absolute bottom-0 right-0 text-xs">
{{ comment.length }}/100
</span>
</div>
// script
data() {
return {
comment: ''
}
}