Custom element based on bootstrap-toggle not updating attribute binding - aurelia

I have created a custom element based on bootstrap-toggle that looks as follows:
toggle.ts:
import {bindingMode, bindable, customElement} from "aurelia-framework";
#customElement('toggle')
export class Toggle {
#bindable({ defaultBindingMode: bindingMode.twoWay }) checked;
input;
attached() {
$(this.input).bootstrapToggle();
}
}
toggle.html:
<template>
<require from="bootstrap-toggle/css/bootstrap2-toggle.min.css"></require>
<require from="bootstrap-toggle"></require>
<input ref="input" data-toggle="toggle" type="checkbox" checked.bind="checked">
</template>
The problem ist that the binding for the checked attribute is never updated when the switch is toggled via the UI. I am aware of the common pitfalls when using Aurelia with jQuery based components as described here. However, in my understanding this should not apply to bootstrap-toggle, as this component triggers a change event on the input element on toggle. I have verified that this change event bubbles up to my custom component.
The workaround I currently use is this:
toggle.ts:
import {bindingMode, bindable, customElement} from "aurelia-framework";
#customElement('toggle')
export class Toggle {
#bindable({ defaultBindingMode: bindingMode.twoWay }) checked;
input;
attached() {
$(this.input).bootstrapToggle().on('change', (event) => this.checked = event.target.checked);
}
}
However, I do not understand why this should be necessary.
I have created a test project based on Aurelia's navigation-skeleton that can be downloaded here.
I would appreciate some help in understanding this!

The event listener used by jquery uses the capture phase and presumably does preventDefault or something among those lines that prevents the event from bubbling back up.
The change event dispatched by this line: if (!silent) this.$element.change() does not actually propagate to the bubble phase. Aurelia uses the bubble phase and thus never receives the event.
You can see the difference if you dispatch one manually in your browser console like so:
document.querySelector("[data-toggle]").dispatchEvent(new CustomEvent("change"));
This will result in the appropriate listeners being invoked and updates the bound property.
What this line does: .on('change', (event) => this.checked = event.target.checked); is also add a listener on the capture phase which then of course works. But then you might as well remove the checked.bind='checked' because that effectively does nothing.
There isn't necessarily any straight-forward fix for this, these CSS frameworks tend to just have very intrusive javascript. I would generally recommend against including their scripts and look for (or implement) them natively in Aurelia. It's very easy to do so and just works a lot better.

Related

Prevent DOM reuse within lit-html/lit-element

I am looking for a way to NOT reuse DOM elements within lit-html/lit-element (yes, I know, I'm turning off one of the prime features). The particular scenario is moving an existing system to lit-element/lit-html that at certain points embeds the trumbowyg WYSIWYG editor. This editor attaches itself to a <div> tag made within lit-element and modifies its own internal DOM, but of course lit-html does not know that this has happened, so it will often reuse the same <div> tag instead of creating a new one. I am looking for something similar to the vue.js key attribute (e.g., preventing Vue from aggresively reusing dom-elements)
I feel like the live() directive in lit-html should be useful for this, but that guards against reuse based on a given attribute, and I want to prevent reuse even if all attributes are identical. Thanks!
I have had similar issues with rich text editors and contenteditable - due to how templates update the DOM you don't want that to be part of a template.
You do this by adding a new element with the non-Lit DOM and then adding that to the DOM that Lit does manage:
class TrumbowygEditor
extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({mode: 'open'});
const div = document.createElement('div');
shadow.appendChild(div);
const style = document.createElement('style');
// Add CSS required
shadow.appendChild(style);
$(div).trumbowyg(); //init
}
}
customElements.define('trumbowyg-editor', TrumbowygEditor);
As this is running in a custom element's shadow DOM Lit won't touch it, you can do:
html`
<div>Lit managed DOM</div>
<trumbowyg-editor></trumbowyg-editor>`;
However, you will have to implement properties and events on TrumbowygEditor to add everything you want to pass to or get from the nested jQuery component.
You can add the scripts with import if you can get module versions of jQuery/Trumbowyg (or your build tools support it) or you can add <script> tags to your component, add fallback loading DOM content in the constructor, and then on the load event of the <script> call the $(div).trumbowyg() to init the component.
While messier and more work I'd recommend the latter as both components are large and (thanks to jQuery being built on assumptions that are now 15 years old) need to load synchronously (<script async or <script defer don't work). Especially on slower connections Lit will be ready long before jQuery/Trumbowyg have loaded in, so you want <trumbowyg-editor> to look good (show spinner, layout in the right amount of space etc) while that's happening.
You write that you attach the external library directly to an element managed by lit-html. It sounds like you're doing essentially this:
render(html`<section><div id=target></div></section>`, document.body)
external_lib.render_to(document.querySelector("#target"))
If this is what you do instead try to create your own div, let the external lib render to that div, and finally attach that div to lit-html:
let target_div = document.createElement('div')
render(html`<section>${div}</section>`, document.body)
external_lib.render_to(target_div)
The most up-to-date answer to this problem is to use Lit's built-in keyed directive. This scenario is exactly what it's for:
https://lit.dev/docs/templates/directives/#keyed
Associates a renderable value with a unique key. When the key changes, the previous DOM is removed and disposed before rendering the next value, even if the value—such as a template—is the same.
#customElement('my-element')
class MyElement extends LitElement {
#property()
userId: string = '';
render() {
return html`
<div>
${keyed(this.userId, html`<user-card .userId=${this.userId}></user-card>`)}
</div>`;
}
}

Programatically assign handler for native event in Vue JS?

I am trying to leverage a Vue mixin to add behavior when a native event happens. Using a mixin will allow me to share that across several components. Specifically, when a field component (or button, or checkbox, etc.) has focus, and the Escape key is pressed, the field loses focus.
A similar Stack Overflow question seemed to indicate I could listen for native events (see code comment about multiple events).
However, the Vue Documentation for programmatically adding an event listener using $on says that it will
Listen for a custom event on the current vm...
(Emphasis added)
Unsure if the custom event remark is absolute or based on the context, I have been experimenting. I have been trying to listen for the native keyup event (using the Vue alias keyup.esc) but have had no success. So I am wondering if it is indeed limited to custom events, and if so, why?
You can see my experiment in a code sandbox. The custom event works, the native does not.
The mixin looks like so:
// escape.mixin.js
export default {
created() {
// Custom event
this.$on("custom-event", function() {
console.log("Custom event handled by mixin");
});
// Native event
this.$on(["keyup.esc", "click"], function() {
alert("Native event handled!");
});
}
};
The main point of all this is to be able to add the behavior to a set of components by adding to how the event is handled, without overriding behavior that might also exist on the component. The secondary goal is to provide the behavior by simply adding the mixin, and not having to do component level wiring of events.
So a component script would look something like this:
// VText component
import escapeMixin from "./escape.mixin";
export default {
name: "VText",
mixins: [escapeMixin],
methods: {
onFocus() {
console.log("Has Focus");
this.$emit("custom-event");
}
}
};
Also, I was trying to avoid attaching the listener to the <input> element directly with vanilla JS because the Vue documentation suggested that letting Vue handle this was a good idea:
[When using v-on...] When a ViewModel is destroyed, all event listeners are automatically removed. You don’t need to worry about cleaning it up yourself.
Solution
skirtle's solution in the comment below did the trick. You can see it working in a code sandbox.
Or here's the relevant mixin:
export default {
mounted() {
this.$el.addEventListener("keyup", escapeBlur);
},
beforeDestroy() {
this.$el.removeEventListener("keyup", escapeBlur);
}
};
function escapeBlur(e) {
if (e.keyCode === 27) {
e.target.blur();
console.log("Lost focus");
}
}

bind click event to child component using v-bind

I created a simple Minesweeper game and when it comes to the decision, which cell to render there are three possibilities:
Unrevealed cell
Revealed mine cell
Revealed neutral cell
I created a row component that renders all the cells contained by the row.
<template>
<div>
<component
v-for="(cell, columnIndex) in row"
:key="columnIndex"
v-bind="getCellProps(cell, columnIndex)"
:is="getComponentCell(cell)"
/>
</div>
</template>
<script>
// imports here
export default {
components: {
UnrevealedCell,
RevealedNeutralCell,
RevealedMineCell
},
props: {
row: Array,
rowIndex: Number
},
methods: {
getCellProps: function(cell, columnIndex) {
if(cell.revealed) {
if (cell.isMine) {
return {};
} else {
return {
mineNeighbours: cell.mineNeighbours
};
}
} else {
return {
unrevealedCell: cell,
x: columnIndex,
y: this.rowIndex,
cellClicked: this.onCellClicked
};
}
},
getComponentCell: function(cell) {
if(cell.revealed) {
if (cell.isMine) {
return RevealedMineCell;
} else {
return RevealedNeutralCell;
}
} else {
return UnrevealedCell;
}
},
onCellClicked: function(x, y) {
debugger;
}
}
}
</script>
Unfortunately my cellClicked event is not working. The child component is able to emit the event correctly but my onCellClicked doesn't get executed. I think this is because I can't write
cellClicked: this.onCellClicked
as it would normally be
#cellClicked
Without the # the attribute might get added as a component property. How can I fix this to listen to the emitted cellClicked event?
A few thoughts occur.
Firstly, the reason this isn't working is because v-bind is used to set component props and element attributes. The # prefix is a shorthand for v-on, so it isn't a prop or attribute in this sense, it's a directive in its own right. v-on does support an object version, just like v-bind, so you can do something like v-on="getCellEvents(cell, columnIndex)" and return a suitable object for each cell type. This is probably the cleanest direct answer to your original question. Less clean and less direct answers are also available...
You could implement this by making cellClicked a prop of the child cell and then calling it as a callback function rather than emitting an event. Not saying you should, but you could. That would work with the code you posted above completely unchanged.
Another alternative is just to add the event listener for all cells. Include #cellClicked="onCellCicked" in the template without worrying about the cell type. If the other cell types don't emit that event then nothing will happen. Vue doesn't know what events a component can fire, you can listen for anything.
Further thoughts...
Your cell template is a bit anaemic. I know people generally advise keeping logic out of the template but in your case I'd say you've probably taken it too far and it just makes things harder to understand. There are two ways you could address this:
Rewrite your component to use a render function instead. Templates exist because humans find them easier to read than render functions but in your case you've got all the logic in JavaScript anyway. The template isn't really adding anything and going all-in with a render function would probably be easier to understand than what you have currently.
Move the logic into the template. I don't see any obvious reason not to do it that way from the code you've posted. I'll post an example at the end.
Either of these two approaches would remove the problem you had adding an event listener.
A final thought on the click events is that you could use event propagation to handle them instead. Add a single click listener on a suitable element of the surrounding component and don't listen for events on the cells/rows at all. The single listener could then establish which cell was clicked (potentially fiddly) and whether anything needs to be done about it. While this would increase the coupling between the components I would imagine that it wouldn't really matter as these components aren't really reusable elsewhere anyway. I'm not recommending this as an approach at this stage but it is worth keeping in mind whenever you find yourself creating large numbers of repetitive components that all need the same events. In your scenario it would probably only make sense if you start to run into performance problems, and even then there will likely be better ways to fix such problems.
So, I promised an example of the template approach:
<template>
<div>
<template v-for="(cell, columnIndex) in row">
<unrevealed-cell
v-if="!cell.revealed"
:key="columnIndex"
:unrevealed-cell="cell"
:x="columnIndex"
:y="rowIndex"
#cellClicked="onCellClicked"
/>
<revealed-mine-cell
v-else-if="cell.mine"
/>
<revealed-neutral-cell
v-else
:mineNeighbours="cell.mineNeighbours"
/>
</template>
</div>
</template>
I'm not sure why the UnrevealedCell needs the x and y but if it's just so that it can emit them as part of the event then you might want to consider registering the listener as #cellClicked="onCellClicked(columnIndex, rowIndex)" and then there's no need to emit the co-ordinates from the cell. I also wonder whether you need 3 separate components for these cells. My gut reaction is that one component would be more appropriate with the row component not needing to have any understanding of the individual cells at all.

is it correct global component communication in vue?

i make modal popup components myPopup.vue for global.
and import that in App.vue and main.js
i use this for global, define some object Vue.prototype
make about popup method in Vue.prototype
like, "show" or "hide", any other.
but i think this is maybe anti pattern..
i want to find more best practice.
in App.vue
<div id="app>
<my-popup-component></my-popup-conponent>
<content></content>
</div>
main.js
...
Vue.prototype.$bus = new Vue(); // global event bus
Vue.prototype.$popup = {
show(params) {
Vue.prototype.$bus.$emit('showPopup', params);
},
hide() {
Vue.prototype.$bus.$emit('hidePopup');
}
}
Vue.component('my-popup-component', { ... });
...
myPopup.vue
....
export default {
...
created() {
this.$bus.$on('showPopup', this.myShow);
this.$bus.$on('hidePopup', this.myHide);
}
...
need-popup-component.vue
methods: {
showPopup() {
this.$popup.show({
title: 'title',
content: 'content',
callback: this.okcallback
});
}
}
It seems to be works well, but i don't know is this correct.
Is there any other way?
I was very surprised while reading your solution, but if you feel it simple and working, why not?
I would do this:
Add a boolean property in the state (or any data needed for showing popup), reflecting the display of the popup
use mapState in App.vue to bring the reactive boolean in the component
use v-if or show in App.vue template, on the popup declaration
create a 'showPopup' mutation that take a boolean and update the state accordingly
call the mutation from anywhere, anytime I needed to show/hide the popup
That will follow the vue pattern. Anything in state, ui components reflect the state, mutations mutates the state.
Your solution works, ok, but it doesn't follow vue framework, for exemple vue debug tools will be useless in your case. I consider better to have the minimum of number of patterns in one app, for maintenance, giving it to other people and so on.
You somehow try to create global component, which you might want to consume in your different projects.
Here is how I think I would do this -
How do I reuse the modal dialog, instead of creating 3 separate dialogs
Make a separate modal component, let say - commonModal.vue.
Now in your commonModal.vue, accept single prop, let say data: {}.
Now in the html section of commonModal
<div class="modal">
<!-- Use your received data here which get received from parent -->
<your modal code />
</div>
Now import the commonModal to the consuming/parent component. Create data property in the parent component, let say - isVisible: false and a computed property for the data you want to show in modal let say modalContent.
Now use it like this
<main class="foo">
<commonModal v-show="isVisible" :data="data" />
<!-- Your further code -->
</main>
The above will help you re-use modal and you just need to send the data from parent component.
How do I know which modal dialog has been triggered?
Just verify isVisible property to check if modal is open or not. If isVisible = false then your modal is not visible and vice-versa
How my global dialog component will inform it's parent component about its current state
Now, You might think how will you close your modal and let the parent component know about it.
On click of button trigger closeModal for that
Create a method - closeModal and inside commonModal component and emit an event.
closeModal() {
this.$emit('close-modal')
}
Now this will emit a custom event which can be listen by the consuming component.
So in you parent component just use this custom event like following and close your modal
<main class="foo">
<commonModal v-show="isVisible" :data="data" #close- modal="isVisible = false"/>
<!-- Your further code -->
</main>

Calling method of another router-view's component / Vue.js

I have two <router-view/>s: main and sidebar. Each of them is supplied with a component (EditorMain.vue and EditorSidebar.vue).
EditorMain has a method exportData(). I want to call this method from EditorSidebar on button click.
What is a good way of tackling it?
I do use vuex, but i don't wanna keep this data reactive since the method requires too much computational power.
I could use global events bus, but it doesn't feel right to use it together with vuex (right?)
I could handle it in root of my app by adding event listener to router-view <router-view #exportClick="handleExportData"> and then target editor component, but it does not feel right as well as later i could need 100 listeners.
Is there any good practice for this? Or did i make some mistakes with the way app is set up? Did is overlooked something in documentation?
After two more years of my adventure with Vue I feel confident enough to answer my own question. It boils down to communication between router views. I've presented two possible solutions, I'll address them separately:
Events bus
Use global events bus (but it doesn't feel right to use it together with vuex)
Well, it may not feel right and it is surely not a first thing you have to think about, but it is perfectly fine use-case for event-bus. The advantage of this solution would be that the components are coupled only by the event name.
Router-view event listeners
I could handle it in root of my app by adding event listener to router-view <router-view #exportClick="handleExportData"> and then target editor component, but it does not feel right as well as later i could need 100 listeners.
This way of solving this problem is also fine, buy it couples components together. Coupling happens in the component containing <router-view/> where all the listeners are set.
Big number of listeners could be addressed by passing an object with event: handler mapping pairs to v-on directive; like so:
<router-view v-on="listeners"/>
...
data () {
return {
listeners: {
'event-one': () => console.log('Event one fired!'),
'event-two': () => console.log('The second event works as well!')
}
}
You could create a plugin for handling exports:
import Vue from 'vue'
ExportPlugin.install = function (Vue, options) {
const _data = new Map()
Object.defineProperty(Vue.prototype, '$exporter', {
value: {
setData: (svg) => {
_data.set('svg', svg)
},
exportData: () => {
const svg = _data.get('svg')
// do data export...
}
}
})
}
Vue.use(ExportPlugin)
Using like:
// EditorMain component
methods: {
setData (data) {
this.$exporter.setData(data)
}
}
// EditorSidebar
<button #click="$exporter.exportData">Export</button>