Can you create components in separate files (like angular) for more modular design - dojo

I have been reading/doing the tutorials, but must be missing something. I am coming from an Angular background.
Can you split up html into components like angular? The problem I am having is that I have about 5 complex panels to display on a map. They are currently all in one html file which makes it very large (each panel is pretty complex).
Do you make widgits for each one? Can you use html when creating widgets?

You can define widgets for every component and compose them as you wish. Also you can define html templates as separate files or just inside widget as template string.
Dojo tutorials are a bit confusing so I'll give you an example:
require([
'dojo/_base/declare',
'dijit/_WidgetBase',
'dijit/_TemplatedMixin'],
function(declare, _WidgetBase, _TemplatedMixin) {
const Header = declare([ _WidgetBase, _TemplatedMixin], {
templateString: '<div>I am Header</div>'
})
const App = declare([ _WidgetBase, _TemplatedMixin], {
templateString: '<div><div data-dojo-attach-point="headerNode"></div>I am App</div>',
constructor: function() {
this.header = new Header()
},
postCreate: function() {
this.header.placeAt(this.headerNode)
this.header.startup()
}
})
let app = new App({}, 'app')
})
<script data-dojo-config="async: 1"
src="//ajax.googleapis.com/ajax/libs/dojo/1.10.4/dojo/dojo.js"></script>
<div id="app">
</div>
In this example I defined two widgets: Header and App
You can create instance of your widget from another one and place it in any domNode. In fact there are a number of ways how to make it. It's just one of them.
In snippet I declared both widgets inside of one require function, but you can do it in separate file, like this:
define([
'dojo/_base/declare',
'dijit/_WidgetBase',
'dijit/_TemplatedMixin',
'dojo/text!templateHtml'
], function (declare, _WidgetBase, _TemplatedMixin, template) {
return declare([_WidgetBase, _TemplatedMixin], {
templateString: template
/// code of widget.............
})
In this case your module must return a declare function with your widget's class.
Then you'll be able to require this widget as dependency in another one.

Related

Accessing DOM element in Vuejs component not reliable

In the simplified example below I demonstrate my problem:
I have a for-loop that asynchronously updates myItems.
I want to be able and update accordingly selectableItems by using this.$el.querySelector('selectable-item').
<template>
<div>
<p>selectableItems: {{selectableItems}}</p>
<div v-for="item in myItems" class="selectable-item">item</div>
</div>
</template>
<script>
export default {
name: 'MyComponent',
data() {
return {
myItems: [],
selectableItems: [],
}
},
created(){
// Populate myItems with a delay
self = this
setTimeout(function() {
self.myItems = [1, 2, 3]
}, 1000);
},
mounted(){
// Fetch some of myItems based on a class
this.selectableItems = this.$el.querySelectorAll('.selectable-item')
},
}
</script>
<style scoped lang="scss">
</style>
I've tried many different things I've found online; TickNext, computed, updated, etc. I think I'm doing something fundamentally wrong. But it is important for my case to be able and select DOM elements by a class.
Any help is deeply appreciated.
Updated: More context
Some people asked me for the bigger picture so I give a bit more info here.
Currently I have a big Vue component where the user is able to select elements. I am trying to factor out all this user interaction into a mixin so I can re-use it in other places of my code.
To make re-usability easy I need to be able and simply add a class selectable to any HTML tag in the template. That's the interface and then the mixin does all the magic and populates selectedElements depending on user interaction.
That's why it is important to avoid refs, etc. since then too much logic leaks everywhere and beats the purpose of making the mixin re-usable. Unless I'm missing something.
OK, after trying many different things I've managed to solve this by using a non-reactive intermediate variable. This means that I can't use the variable in a template but that is fine.
export default {
..
// NOT reactive so you can't just use it in your templates.
_selectableItems: [],
updated(){
self._selectableItems = self.$el.querySelectorAll('.selectable-item')
},
..
}

How to remove the dependency on jquery

When I research on how to integrate Plotly with Vue, I found this example:
https://codepen.io/rhamner/pen/MXgWqJ
It meets my requirement, but it requires jquery js, I would like to remove the dependence of jquery js file.
I tried to add bellow code into the component
ref='chart_id'
and in the mounted() I change as bellow:
this.$refs.chart_id.$on('plotly_hover', this.hover);
But looks like it doesn't work.
How to change the code, I just think it should be able to code in another way to exclude jquery to save move time one page loading.
Thanks!
jQuery is only used in mounted for two bindings which can be easily rewritten:
//...
mounted() {
this.Plot();
this.$watch("data", this.Plot, { deep: true });
window.addEventListener("resize", this.onResize);
},
beforeDestroy() {
window.removeEventListener("resize", this.onResize);
},
methods: {
Plot() {
Plotly.newPlot(this.divId, this.data, this.layout);
this.$el.on('plotly_hover', this.hover);
},
//...
}
//...
Since your normal hover event has points inside event, not inside eventData, you also need to modify the hover function to look for points in both places:
hover: function (event, eventData) {
this.$emit('hover', eventData
? eventData.points
: event.points,
this.divId);
},
... to cover both cases.
See it working without jQuery here: https://codepen.io/andrei-gheorghiu/pen/XWmJbYo

Nuxt reusable dynamic custom page transition with javascript hooks?

I have a Nuxt.js site that I'm trying to get some fancy page transitions working on. I think I understand how you're supposed to use the transition setting when it's just CSS, but how do I make it reusable with JavaScript hooks?
It seems to me we should be able to do something like this:
// In a Page.vue template
transition(to, from) {
if (!from) {
return "fade"
}
if (to.name == "directors-name-work") {
// Animate to video playing
return "to-video"
}
if (from.name == "directors-name-work") {
// Scroll to slideshow, and at same video we just came from.
return "from-video"
}
}
And then I need to be able to define what the JS hooks are for to-video and from-video in JavaScript somewhere, but I have no idea where that goes? Where does enter() and beforeEnter() hooks get defined for the separate transitions? It makes sense if we just have one transition, then I could do it in a mixin. But when it is dynamic I have no idea.
Is there a file I should be putting somewhere called transition-to-video and transition-from-video?
It's currently undocumented, but the page's transition function can return a transition object, which may include the transition JavaScript hooks. This allows you to define your shared transition objects in a common file, and import them into a page as needed:
~/transitions.js:
export default {
fade: {
name: 'fade',
mode: 'out-in',
beforeEnter(el) {
console.log('fade beforeEnter')
}
},
bounce: {
name: 'bounce',
afterEnter(el) {
console.log('bounce afterEnter')
}
},
}
~/pages/about.vue:
<script>
import transitions from '~/transitions'
export default {
transition(to, from) {
return to.query.fade ? transitions.fade : transitions.bounce
},
}
</script>

How to create router-link programmatically on html render using Datatables.net + Vue.js?

I have a Datatables.net jquery plugin as a vue component:
DatatablesCGU:
<template>
<table v-bind="$props" ref="tableElement" class="table table-striped table-hover table-bordered">
<slot></slot>
</table>
</template>
<script>
import $ from 'jquery';
// Datatables
require('datatables.net-bs');
require('datatables.net-bs4/js/dataTables.bootstrap4.js');
require('datatables.net-buttons');
require('datatables.net-buttons-bs');
require('datatables.net-responsive');
require('datatables.net-responsive-bs');
require('datatables.net-responsive-bs/css/responsive.bootstrap.css');
require('datatables.net-buttons/js/buttons.colVis.js'); // Column visibility
require('datatables.net-buttons/js/buttons.html5.js'); // HTML 5 file export
require('datatables.net-buttons/js/buttons.flash.js'); // Flash file export
require('datatables.net-buttons/js/buttons.print.js'); // Print view button
require('datatables.net-keytable');
require('datatables.net-keytable-bs/css/keyTable.bootstrap.css');
require('datatables.net-select');
require('jszip/dist/jszip.js');
require('pdfmake/build/pdfmake.js');
require('pdfmake/build/vfs_fonts.js');
//Evita o alert chato do datatables em caso de erro
$.fn.dataTable.ext.errMode = function ( settings, helpPage, message ) {
console.error(message);
};
/**
* Wrapper component for dataTable plugin
* Only DOM child elements, componets are not supported (e.g. <Table>)
*/
export default {
name: 'DatatableCGU',
props: {
/** datatables options object */
options: { type: Function, "default": ()=>{} },
/** callback that receives the datatable instance as param */
dtInstance: Function
},
data(){
return { datatables : null}
},
mounted() {
const dtInstance = $(this.$refs.tableElement).DataTable(this.options());
this.datatables = dtInstance;
if (this.dtInstance) {
this.dtInstance(dtInstance);
}
this.$root.$on('filtrar', this.refresh);
},
destroyed() {
$(this.$refs.tableElement).DataTable({destroy: true});
},
methods: {
refresh(filtros) {
this.datatables.ajax.reload();
}
}
}
</script>
On another component, i use this passing a datatables options with some custom renders on columns properties:
...
methods: {
getOptions(){
let options = this.getDefaultOptions();
options.ajax.url = "/api/auth/usuarios";
options.filtrador = this.filtrador;
options.columns = [
this.colunaDeSelecao(this.modoPopup)
,{name: "cpf", data: "cpf", title: "CPF"}
,{name: "nome", data: "nome", title: "Nome"}
,{name: "email", data: "email", title: "E-mail"}
,{name: "id", data: "id", title: "Ações", visible: !(this.modoPopup), sortable:false, className:"dt-center", width: "200px", render: function(data, type, row) {
return `<span class='btn-group btn-group-sm'>
<button id='btnAlternar__${data}' data-id='${data}' class='btn btn-${row.ativo?"danger":"success"}' data-toggle='tooltip' title='${row.ativo?"Inativar":"Ativar"}'><i class='fas fa-power-off'></i></button>
<a href='${window.$baseURL}auth/usuarios/${data}' class='btn btn-warning' data-toggle='tooltip' title='Editar'><i class='far fa-edit'></i></a>
</span>`;
}}
];
options.initComplete = () =>{
this.getDefaultOptions().initComplete();
this.criarTogglersSituacao();
};
return options;
}
...
If you notice the last column render creates a <a href='${window.$baseURL}auth/usuarios/${data}' ... that obviously isn't a router-link and doesn't trigger vue router properly, causing an undesired page refresh.
I need the link to do a router push instead of a page refresh. How is this possible?
There is no good answer to that problem. datatables is not really compatible with Vue.js. With Vue, the usual way to go would be to pass your reactive HTML structure within a slot to such a library. Because datatables requires you to use a render function and return static HTML as a string, you cannot pass any JavaScript logic along.
The main problem is that you need to pass an event from the link tag to the Vue component. One approach would be to pass HTML in the render function which then can be selected with a specific selector (e.g. adding a class). You also need to add the link data/the item's ID to the HTML element (e.g. with a data-link="" attribute). When datatables has finished rendering, you can add a click listener to all the links. This click listener handler function needs to read the link/ID of the link and pass it to the router. Then, you can use Vue's router.push() function.
When you are implementing a solution with the above approach, make sure to assign and remove the click listeners depending on the lifecycle events of datatables. It might be necessary to add and remove the listeners on each page switch.

Dynamically insert child components inside vuejs2 data (without $compile or abusing v-html)

I'd like to insert new vuejs components on the fly, at arbitrary points within a block of not-necessarily-predefined HTML.
Here's a slightly contrived example that demonstrates the sort of thing I'm trying to do:
Vue.component('child', {
// pretend I do something useful
template: '<span>--><slot></slot><--</span>'
})
Vue.component('parent', {
data() {
return {
input: 'lorem',
text: '<p>Lorem ipsum dolor sit amet.</p><p><i>Lorem ipsum!</i></p>'
}
},
template: `<div>
Search: <input type='text' v-model="input"><br>
<hr>
This inserts the child component but doesn't render it
or the HTML:
<div>{{output}}</div>
<hr>
This renders the HTML but of course strips out the child component:
<div v-html="output"></div>
<hr>
(This is the child component, just to show that it's usable here:
<child>hello</child>)
<hr>
This is the goal: it renders both the input html
and the inserted child components:
TODO ¯\_(ツ)_/¯
</div>`,
computed: {
output() {
/* This is the wrong approach; what do I replace it with? */
var out = this.text;
if (this.input) {
this.input = this.input.replace(/[^a-zA-Z\s]/g,'');
var regex = new RegExp(this.input, "gi");
out = out.replace(regex, '<child><b>' + this.input + '</b></child>');
}
return out;
}
}
});
new Vue({
el: '#app'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.0/vue.js"></script>
<div id="app">
<parent></parent>
</div>
In the above snippet, assume data.text is sanitized HTML. <child> is some sub-component that does something useful, which I want to wrap around chunks of data.text that aren't known ahead of time. (input is just for demo here. This MCVE doesn't really resemble the code I'm building, it's just an example that shows the sort of situation I'm stuck on.)
So: how would I change either the output function or the parent component's template, such that both the HTML from input and the inserted <child> templates are rendered properly?
What I've tried
In Vue 1, the answer to this would be a straightforward $compile. I'm using vuejs2 which removed $compile (out of justifiable concern that it made it too easy to naively introduce XSS vulnerabilities.)
v-html sanitizes what you feed it, which strips the child component out. Obviously this is not the way to do this. (That page suggests using partials instead, but I'm not sure how that could be applied to this situation; in any case partials have also been removed from vue2.)
I've tried passing the results of output() into another component which would then use it as its template. This seems like a promising approach, but I can't figure out how to change that secondary component's template. template only accepts a string, not a function like many of the other component properties, so I can't pass the template html in, say, a prop. Something like rewriting this.template inside beforeMount() or bind() would have been nice, but no joy there either. Is there some other way to replace a component's template string before it's mounted?
Unlike template, I can pass data to a component's render() function... but then I'm still stuck having to parse that html string into nested createElement functions. Which is exactly what Vue is doing internally in the first place; is there some way to hook into that here short of reinventing it myself?
Vue.component('foo', {
props: ['myInput'],
render(createElement) {
console.log(this.myInput); // this works...
// ...but how to parse the html in this.myInput into a usable render function?
// return createElement('div', this.myInput);
},
})
I wasn't able to cheat my around this with inline-template, either: <foo inline-template>{{$parent.output}}</foo> does exactly the same thing as a plain old {{output}}. In retrospect that should have been obvious, but it was worth a shot.
Maybe constructing an async component on the fly is the answer? This could clearly generate a component with an arbitrary template, but how would I reasonably call that from the parent component, and feed output to the constructor? (It would need to be reusable with different input, with multiple instances potentially visible simultaneously; no globals or singletons.)
I've even considered ridiculous stuff like having output() split the input into an array at the points where it would have inserted <child>, and then doing something like this in the main template:
...
<template v-for="chunk in output">
<span v-html="chunk"></span>
<child>...</child>
</template>
....
That would be doable, if laborious -- I'd have to split out what goes in the child's slot into a separate array too and get it by index during the v-for, but that could be done... if input were plain text instead of HTML. In splitting HTML I'll often wind up with unbalanced tags in each chunk, which can mess up the formatting when v-html rebalances it for me. And anyway this whole strategy feels like a bad hack; there must be a better way.
Maybe I just drop the whole input into a v-html and then (somehow) insert the child components at the proper positions through after-the-fact DOM manipulation? I haven't explored this option too deeply because it, too, feels like a hack, and the reverse of the data-driven strategy, but maybe it's a way to go if all else fails?
A couple of pre-emptive disclaimers
I'm very well aware of the XSS risks involved in $compile-like operations. Please be assured that none of what I'm doing involves unsanitized user input in any way; the user isn't inserting arbitrary component code, instead a component needs to insert child components at user-defined positions.
I'm reasonably confident that this is not an XY problem, that I really do need to insert components on the fly. (I hope it's obvious from the number of failed attempts and blind alleys I've run down that I've put more than a little thought into this one!) That said, if there's a different approach that leads to similar results, I'm all ears. The salient point is that I know which component I need to add, but I can't know ahead of time where to add it; that decision happens at run time.
If it's relevant, in real life I'm using the single-file component structure from vue-cli webpack template, not Vue.component() as in the samples above. Answers that don't stray too far from that structure are preferred, though anything that works will work.
Progress!
#BertEvans points out in comments that Vue.compile() is a thing that exists, which is an I-can't-believe-I-missed-that if ever there was one.
But I'm still having trouble using it without resorting to global variables as in that documentation. This renders, but hardcodes the template in a global:
var precompiled = Vue.compile('<span><child>test</child></span>');
Vue.component('test', {
render: precompiled.render,
staticRenderFns: precompiled.staticRenderFns
});
But various attempts to rejigger that into something that can accept an input property have been unsuccessful (the following for example throws "Error in render function: ReferenceError: _c is not defined", I assume because the staticRenderFns aren't ready to go when render needs them?
Vue.component('test', {
props: ['input'],
render() { return Vue.compile(this.input).render()},
staticRenderFns() {return Vue.compile(this.input).staticRenderFns()}
});
(It's not because there are two separate compile()s -- doing the precompile inside beforeMount() and then returning its render and staticRenderFns throws the same error.)
This really feels like it's on the right track but I'm just stuck on a dumb syntax error or the like...
As mentioned in the my comment above, $compile was removed, but Vue.compile is available in certain builds. Using that below works as I believe you intend except in a couple cases.
Vue.component('child', {
// pretend I do something useful
template: '<span>--><slot></slot><--</span>'
})
Vue.component('parent', {
data() {
return {
input: 'lorem',
text: '<div><p>Lorem ipsum dolor sit amet.</p><p><i>Lorem ipsum!</i></p></div>'
}
},
template: `<div>
Search: <input type='text' v-model="input"><br>
<hr>
<div><component :is="output"></component></div>
</div>`,
computed: {
output() {
if (!this.input)
return Vue.compile(this.text)
/* This is the wrong approach; what do I replace it with? */
var out = this.text;
if (this.input) {
this.input = this.input.replace(/[^a-zA-Z\s]/g,'');
var regex = new RegExp(this.input, "gi");
out = out.replace(regex, '<child><b>' + this.input + '</b></child>');
out = Vue.compile(out)
}
return out;
}
}
});
new Vue({
el: '#app'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.0/vue.js"></script>
<div id="app">
<parent></parent>
</div>
You mentioned you are building with webpack and I believe the default for that build is Vue without the compiler, so you would need to modify it to use a different build.
I added a dynamic component to accept the results of the compiled output.
The sample text is not a valid template because it has more than one root. I added a wrapping div to make it a valid template.
One note: this will fail if the search term matches all or part of any of the HTML tags in the text. For example, if you enter "i", or "di" or "p" the results will not be what you expect and certain combinations will throw an error on compilation.
I'm posting this as a supplement to Bert Evans's answer, for the benefit of vue-cli webpack users who want to use .vue files instead of Vue.component(). (Which is to say, I'm mostly posting this so I'll be able to find this information when I inevitably forget it...)
Getting the right Vue build
In vue-cli 2 (and possibly 1?), to ensure Vue.compile will be available in the distribution build, confirm webpack.base.conf.js contains this line:
'vue$': 'vue/dist/vue.esm.js' // or vue/dist/vue.common.js for webpack1
instead of 'vue/dist/vue.runtime.esm.js'. (If you accepted the defaults when running vue init webpack you will already have the full standalone build. The "webpack-simple" template also sets the full standalone build.)
Vue-cli 3 works somewhat differently, and does not have Vue.compile available by default; here you'll need to add the runtimeCompiler rule to vue.config.js:
module.exports = {
/* (other config here) */
runtimeCompiler: true
};
The component
The "child" component can be a normal .vue file, nothing special about that.
A bare-bones version of the "parent" component would be:
<template>
<component :is="output"></component>
</template>
<script>
import Vue from 'vue';
import Child from './Child'; // normal .vue component import
export default {
name: 'Parent',
computed: {
output() {
var input = "<span>Arbitrary single-root HTML string that depends on <child></child>. This can come from anywhere; don't use unsanitized user input though...</span>";
var ret = Vue.compile(input);
ret.components = { Child }; // add any other necessary properties similarly
ret.methods = { /* ... */ } // like so
return ret;
}
}
};
</script>
(The only significant difference between this and the non-webpack version is importing the child, then declaring the component dependencies as ret.components: {Child} before returning it.)