Custom directive - vue.js

The documentation for custom directives demonstrates using a dynamic argument and a value together:
Directive arguments can be dynamic. For example, in v-mydirective:[argument]="value", the argument can be updated based on data properties in our component instance! This makes our custom directives flexible for use throughout our application.
If "value" doesn't contain a space, it works fine. But adding a space to the value (e.g. v-mydirective:[argument]="some value") causes Nuxt to choke:
invalid expression: Unexpected identifier in
some value
Raw expression: v-mydirective:[argument]="some value"
What is the problem, and how do I resolve it so that I can use a string with a space as the value to the custom directive?

Issue:
This happens because when we pass value with spaces, the expression is evaluated by vuejs and it tries to find the data options with property some & value. But as none exists with those property names, hence we get the mentioned error.
A simple example to explain this is when we pass value as:
v-mydirective:[argument]="2"
and if we do console.log inside bind function:
console.log(binding.value)
You will see the output displayed as 2. But, if we pass value as:
v-mydirective:[argument]="2 + 2"
and if we do console.log inside bind function, interestingly the output displayed this time is 4 instead of 2 + 2
Solutions:
There are two possible solutions for this:
Solution #1:
You can simply wrap some value in single quotes and pass it as a string like:
v-mydirective:[argument]="'some value'"
This way the expression will be directly evaluated as string instead.
Demo:
Vue.directive('pin', {
bind: function (el, binding, vnode) {
console.log(binding.value)
}
})
new Vue({
el: '#dynamicexample',
data: function () {
return {
direction: 'left',
}
}
})
#dynamicexample {
font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif;
color: #304455;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="dynamicexample">
<p v-pin:[direction]="'some value'">I am pinned onto the page at 200px to the left.</p>
</div>
Solution #2:
You can also create a separate data option for it like:
data: function () {
return {
myValue: 'some value'
}
}
and then you can use it in directive like:
v-mydirective:[argument]="myValue"
Demo:
Vue.directive('pin', {
bind: function (el, binding, vnode) {
console.log(binding.value)
}
})
new Vue({
el: '#dynamicexample',
data: function () {
return {
direction: 'left',
myValue: 'some value'
}
}
})
#dynamicexample {
font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif;
color: #304455;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="dynamicexample">
<p v-pin:[direction]="myValue">I am pinned onto the page at 200px to the left.</p>
</div>

Related

Set CSS class dynamically without template

For each component with prefix mycomponent- I would like to add a class with the name of the component. I don't want to have to modify the component in order to do this.
My first thought was to use mixins and somehow add the class in beforeCreate but I haven't managed to add classes dynamically without using the template.
Do I have to use $el.classList.add(this.$options.name) in beforeUpdate or similar? Is there some more Vue-ish way to do it?
Here it is, wrapped up as plugin:
const addComponentNameAsClass = {
install(Vue, options) {
const fn = Vue.prototype.$mount;
Vue.prototype.$mount = function() {
fn.apply(this, arguments);
if (this.$options._componentTag?.startsWith("mycomponent-")) {
this.$el.classList.add(this.$options._componentTag);
}
}
}
}
Vue.use(addComponentNameAsClass);
// that's all you need
// see it working:
['a', 'b', 'foo', 'whatever'].forEach(type => {
Vue.component('mycomponent-' + type, {
template: '<div><slot /></div>'
})
});
new Vue({
el: '#app'
})
[class^="mycomponent-"] {
border: 1px solid;
margin-bottom: 4px;
padding: 1rem;
}
.mycomponent-a {
border-color: red;
}
.mycomponent-b {
border-color: blue;
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.12"></script>
<div id="app">
<mycomponent-a>I should get a red border.</mycomponent-a>
<mycomponent-b>I should get a blue one.</mycomponent-b>
<mycomponent-foo>bar</mycomponent-foo>
<mycomponent-whatever>Meh.</mycomponent-whatever>
</div>
Notes:
you should not expect this to work on Vue3. Why? Because whenever you're using internal props starting with _ Vue does not guarantee they'll still be there in the next major version update. But, on the other hand, the name of the component is not saved anywhere else (other than $options._componentTag).
the above won't work if you use components as <MycomponentA></MycomponentA>. However, you can swiftly get around it by running the value of $options._componentTag through a helper function (e.g: kebabCase from lodash).
note on note: if you want the added class to always be kebab-case, you'll have run the value passed to .classList.add() through kebabCase, as well). Otherwise, <MycomponentA> will add MycomponentA class and <mycomponent-a> will add mycomponent-a class, for obvious reasons.
Ref. "Vue-ish way": whatever the end goal of applying this "component" class is, chances are it can be achieved cleaner.
The very idea of placing classes denominating component type doesn't feel Vue-ish at all.
It feels WordPress-ish and Angular-ish. To me, at least.

Why does variable substitution not work for my case?

I'm trying to create a custom widget using templates, but variable substitution does not seem to be working for me.
I can see the property value being updated inside the widget, but the DOM does not change. For example, when I use the get() method, I can see the new value of the widget's property. However, the DOM never changes its value.
Here is my template:
<div class="outerContainer">
<div class="container">
<span class="mySpan">My name is ${name}</span>
</div>
</div>
Now, here is my widget code:
define([
"dojo/_base/declare",
"dijit/_WidgetBase",
"dijit/_TemplatedMixin",
"dojo/text!/templates/template.html",
], function (declare, _WidgetBase, _TemplatedMixin, template) {
return declare([_WidgetBase, _TemplatedMixin], {
templateString: template,
name: "",
constructor: function (args) {
console.log("calling constructor of the widget");
this.name = args.name;
},
startup: function() {
this.inherited(arguments);
this.set("name", "Robert"); // this does not work
},
postCreate: function() {
this.inherited(arguments);
this.set("name, "Robert"); // this does not work either
},
_setNameAttr: function(newName) {
// I see this printed in the console.
console.log("Setting name to " + newName);
this._set("name", newName);
// I also see the correct value when I get()
console.log(this.get("name")); // This prints Robert
}
});
});
I was expecting the DOM node to say "My name is Robert" i.e. the new value, but it never updates. Instead, it reads "My name is ". It does not overwrite the default value.
I'm sure I'm missing a silly step somewhere, but can someone help me figure out what?
You should bind the property to that point in the dom. So you will need to change the template to this:
<span class="mySpan">My name is <span data-dojo-attach-point='nameNode'></span></span>
And in your widget you should add this function to bind it whenever name changes:
_setNameAttr: { node: "nameNode", type: "innerHTML" },
Now when name changes, it will change the innerHTML of the nameNode inside your mySpan span. If you need to know more about this binding I recommend checking the docs out.
Hope this helps!

Vue.JS value tied on input having the focus

Is there a way to change a value in the model when an input gets/loses focus?
The use case here is a search input that shows results as you type, these should only show when the focus is on the search box.
Here's what I have so far:
<input type="search" v-model="query">
<div class="results-as-you-type" v-if="magic_flag"> ... </div>
And then,
new Vue({
el: '#search_wrapper',
data: {
query: '',
magic_flag: false
}
});
The idea here is that magic_flag should turn to true when the search box has focus. I could do this manually (using jQuery, for example), but I want a pure Vue.JS solution.
Apparently, this is as simple as doing a bit of code on event handlers.
<input
type="search"
v-model="query"
#focus="magic_flag = true"
#blur="magic_flag = false"
/>
<div class="results-as-you-type" v-if="magic_flag"> ... </div>
Another way to handle something like this in a more complex scenario might be to allow the form to track which field is currently active, and then use a watcher.
I will show a quick sample:
<input
v-model="user.foo"
type="text"
name="foo"
#focus="currentlyActiveField = 'foo'"
>
<input
ref="bar"
v-model="user.bar"
type="text"
name="bar"
#focus="currentlyActiveField = 'bar'"
>
...
data() {
return {
currentlyActiveField: '',
user: {
foo: '',
bar: '',
},
};
},
watch: {
user: {
deep: true,
handler(user) {
if ((this.currentlyActiveField === 'foo') && (user.foo.length === 4)) {
// the field is focused and some condition is met
this.$refs.bar.focus();
}
},
},
},
In my sample here, if the currently-active field is foo and the value is 4 characters long, then the next field bar will automatically be focused. This type of logic is useful when dealing with forms that have things like credit card number, credit card expiry, and credit card security code inputs. The UX can be improved in this way.
I hope this could stimulate your creativity. Watchers are handy because they allow you to listen for changes to your data model and act according to your custom needs at the time the watcher is triggered.
In my example, you can see that each input is named, and the component knows which input is currently focused because it is tracking the currentlyActiveField.
The watcher I have shown is a bit more complex in that it is a "deep" watcher, which means it is capable of watching Objects and Arrays. Without deep: true, the watcher would only be triggered if user was reassigned, but we don't want that. We are watching the keys foo and bar on user.
Behind the scenes, deep: true is adding observers to all keys on this.user. Without deep enabled, Vue reasonably does not incur the cost of maintaining every key reactively.
A simple watcher would be like this:
watch: {
user() {
console.log('this.user changed');
},
},
Note: If you discover that where I have handler(user) {, you could have handler(oldValue, newValue) { but you notice that both show the same value, it's because both are a reference to the same user object. Read more here: https://github.com/vuejs/vue/issues/2164
Edit: to avoid deep watching, it's been a while, but I think you can actually watch a key like this:
watch: {
'user.foo'() {
console.log('user foo changed');
},
},
But if that doesn't work, you can also definitely make a computed prop and then watch that:
computed: {
userFoo() {
return this.user.foo;
},
},
watch: {
userFoo() {
console.log('user foo changed');
},
},
I added those extra two examples so we could quickly note that deep watching will consume more resources because it triggers more often. I personally avoid deep watching in favour of more precise watching, whenever reasonable.
However, in this example with the user object, if all keys correspond to inputs, then it is reasonable to deep watch. That is to say it might be.
You can use a flat by determinate a special CSS class, for example this a simple snippet:
var vm = new Vue({
el: '#app',
data: {
content: 'click to change content',
flat_input_active: false
},
methods: {
onFocus: function(event) {
event.target.select();
this.flat_input_active = true;
},
onBlur: function(event) {
this.flat_input_active = false;
}
},
computed: {
clazz: function() {
var clzz = 'control-form';
if (this.flat_input_active == false) {
clzz += ' only-text';
}
return clzz;
}
}
});
#app {
background: #EEE;
}
input.only-text { /* special css class */
border: none;
background: none;
}
<!-- libraries -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<!-- html template -->
<div id='app'>
<h1>
<input v-model='content' :class='clazz'
#focus="onFocus($event)"
#blur="onBlur"/>
</h1>
<div>
Good luck
You might also want to activate the search when the user mouses over the input - #mouseover=...
Another approach to this kind of functionality is that the filter input is always active, even when the mouse is in the result list. Typing any letters modifies the filter input without changing focus. Many implementations actually show the filter input box only after a letter or number is typed.
Look into #event.capture.

Custom attribute not working on dynamic content

I'm using w2ui grid, and the template column generated like so:
{ field: 'TableCards', caption: 'Table cards', size: '10%', sortable: true ,
render:function(record, index, column_index) {
let html = '';
if (record.TableCards) {
record.TableCards.forEach(function(card) {
html += `<div class="card-holder" style="width: 12%; display: inline-block; padding: 0.5%;">
<div class="poker-card blah" poker-card data-value="${card.value}"
data-color="${card.color}"
data-suit="&${card.suit};"
style="width: 30px;height: 30px">
</div>
</div>`;
});
}
return html;
}
},
poker-card as u can see is a custom attribute. and it's not get rendered in the grid.
any other way?
You can use the TemplatingEngine.enhance() on your dynamic HTML.
See this article for a complete example: http://ilikekillnerds.com/2016/01/enhancing-at-will-using-aurelias-templating-engine-enhance-api/
Important note: based on how your custom attribute is implemented, you may need to call the View's lifecycle hooks such as .attached()
This happened to me, when using library aurelia-material, with their attribute mdl.
See this source where the MDLCustomAttribute is implemented, and now see the following snippet, which shows what I needed to do in order for the mdl attribute to work properly with dynamic HTML:
private _enhanceElements = (elems) => {
for (let elem of elems) {
let elemView = this._templEngine.enhance({ element: elem, bindingContext: this});
//we will now call the View's lifecycle hooks to ensure proper behaviors...
elemView.bind(this);
elemView.attached();
//if we wouldn't do this, for example MDL attribute wouldn't work, because it listens to .attached()
//see https://github.com/redpelicans/aurelia-material/blob/5d3129344e50c0fb6c71ea671973dcceea14c685/src/mdl.js#L107
}
}

how to put html tag in label columnheader?

I use dgrid to make a simple grid (http://dojofoundation.org/packages/dgrid/tutorials/defining_grid_structures/).
My question is simple : how to put html tag in label columnheader's ? Because if I put an img tag for example, label contains the string img src=...
Thanks
The column definition can provide a function that builds the column header.
var column = {
//...
renderHeaderCell: function(node) {
domConstruct.create('img', {src: ''}, node);
return node;
}
};
See the documentation of the renderHeaderCell() function in the DGrid wiki:
renderHeaderCell(node)
An optional function that will be called to render the column's header
cell. Like renderCell, this may either operate on the node directly,
or return a node to be placed within it.
One-line answer using put-selector:
renderHeaderCell: function(node) {
return put("img[src=/your/image]");
}
Note this function won't work if your column happens to be a selector - because selector.js defines his own renderHeaderCell(node) function.
#craig Thanks for the answer, in my case I only needed to know how to add HTML into the header cell and the renderHeaderCell(node) was definitely the answer.
For anyone else simply needing to add a <br>, <span>, <div> etc to the header cell, here's a couple of simple examples to compare:
Example without using renderHeaderCell(node):
{
label: 'Title',
field: appConfig.fields[0],
sortable: false
}
Example using renderHeaderCell(node):
{
renderHeaderCell: function(node) {
node.innerHTML = '<span class="headerCell">Title<br><br></span>'
},
field: appConfig.fields[0],
sortable: false
}
Then you can target with CSS as normal:
.headerCell {
font-size: 9px;
}