Problems with custom renderer and validation triggered in custom elements - aurelia

I'm using aurelia-validation#1.0.0-beta.1.0.1.
My scenario is:
I've got a form which has a validation controller and validation rules
There is a containerless custom element in the form which wraps a password input, and exposes the current password as a bindable property
The validate binding behavior is used on the form's binding to this custom element property
The custom element also raises the blur event, so that the validation binding is triggered when the wrapped password input loses focus
The validation lifecycle is working as expected.
The problem I'm running into is with the custom renderer I'm using, which currently assumes the element it receives is the actual DOM input element so that a class can be applied to the input, and a sibling error element can be injected next to it, but here it's receiving the custom element that wraps the input, which can't be handled in the same manner because it's just a comment node in the DOM.
Is there a strategy or API in aurelia-validation that could solve this sort of problem? I'm stumped, and can't find much out there on working with custom elements within validation.
EDIT:
Here is the custom element template:
<template>
<div class="input-group -password">
<div class="input-toggle-wrapper">
<label for="password" class="-hidden" t="fields_Password"></label>
<input
id="password"
type="${isPasswordVisible ? 'text' : 'password'}"
value.bind="password"
t="[placeholder]fields_Password"
maxlength="20"
focus.trigger="onInputFocus()"
blur.trigger="onInputBlur()" />
<div
class="toggle ${isPasswordVisible ? '-show' : ''}"
click.delegate="onToggleClick($event)"
mousedown.delegate="onToggleMouseDown($event)"></div>
</div>
</div>
</template>
I made it containerless because I don't want <password-box> emitted into the DOM as an outer element, as that breaks the current CSS rules for layout (and I don't want to change the CSS).
However if the custom element is containerless then I don't know how to access the first div inside the template using DOM navigation from the comment node that represents the custom element in the DOM.

Unfortunately, Aurelia team has identified this issue and (at least as of now) said they won't fix it. https://github.com/aurelia/templating/issues/140
There is a hacky workaround for the issue as follows:
if(element.nodeType === 8) {
element = this.getPreviusElementSibling(element)
}
If you add that within your render method for your renderer, it should work. Again, hacky, but it gets the job done in lieu of an official fix from the AU team.

Related

How to change HTML tags of the component dynamically after click in Vue3 composition-api?

I am writing my first app in Vue3 and I use composition-api with script setup.
Using v-for, I create components that are inputs (CrosswordTile) that make up the crossword grid.
A problem appeared during the implementation of the field containing a clue to the password.
Since the text doesn't allow text to wrap, I wanted to dynamically change the tag to after a click.
Function in parent component where I handle logic after click that change tile type works fine, but I need to change tag of "target" to and set maxLength to a different value.
If it would help here is whole code on github: https://github.com/shadowas-py/lang-cross/tree/question-tile, inside CrosswordGrid.vue.
function handleTileTypeChange(target: HTMLInputElement) {
if (target && !target.classList.contains('question-field')) {
addStyle(target, ['question-field']);
iterateCrosswordTiles(getNextTile.value(target), removeStyle, ['selected-to-word-search', 'direction-marking-tile']);
} else if (target) {
removeStyle(target, ['question-field']);
if (getPrevTile.value(target)?.classList.contains('direction-marking-tile')) {
iterateCrosswordTiles(
target,
addStyle,
['selected-to-word-search', 'direction-marking-tile'],
);
}
}
TEMPLATE of ParentComponent
<div
class="csw-grid"
#input="handleKeyboardEvent($event as any)"
#mousedown.left.stop="handleClickEvent($event)"
#click.stop="">
<div v-for="row in 10" :key="row" class="csw-row" :id="`csw-row-${row}`">
<CrosswordTile
v-for="col in 8"
:key="`${col}-${row}`"
#click.right.prevent='handleTileTypeChange($event.target)'
/>
</div>
</div>
I tried to use v-if inside CrosswordTile, but it creates a new element, but I just need to modify the original one (to add/remove HTML classes from it basing on logic inside CrosswordGrid component).
How can I get access to the current component instance properties when using the composition API in script setup or how to replace the tag dynamically?
:is and is doesn't work at all.

vue-tippy with a single global popup for multiple buttons

I'm using vue-tippy 6.0.0-alpha.63 for vue3.
I have a single component on the page which has a dynamic content and want to make a tooltip from it. This tooltip should be shown when user moves mouse over one of many buttons with class skill_tab. Those buttons are nested deeply inside the parent component of the current component.
I wrapped the component content with a <tippy> component and I'm trying to bind the tooltip to the buttons using to prop but it doesn't work.
I also tried using the triggerTarget prop providing the class .skill_tab as the value, but it looks like triggerTarget only works with refs.
Component for a button:
SkillTab.Vue
<div class="skill_tab">
...
</div>
Component for the tooltip:
SkillTooltip.Vue
<tippy followCursor=true to=".skill_tab" allowHtml=true placement="bottom-end" interactive=true>
<div id="skill_tooltip" class="skill_tooltip" :class="{active}" v-if="active">
...
</div>
</tippy>
Is there any way to bind the tooltip to all the buttons without passing refs via a global state or any other simple way?

How to specify two-way bindable property in HTML-only custom element (Aurelia)

I have created an HTML-only custom element to represent an input and all the DOM structure that does with it.
I have set up bindable properties on the template to supply values into the element. However, I don't see a way to specify that a bindable should be two-way.
the element:
<template bindable="label,name,placeholder,value">
<div class="form-group">
<label class="control-label col-sm-2" for.bind="name">${label}</label>
<div class="col-sm-7 col-md-6">
<input class="form-control" id.bind="name" placeholder.bind="placeholder" value.bind="value" />
</div>
</div>
</template>
I know I can specify two-way binding each time the element is used (e.g. <my-element value.bind="firstName & twoWay"></my-element>, but I want to set the default without having to create and maintain a separate class (i.e. I like html-only element for this case).
Is this possible?
I don't think that's possible in a simple way. I mean, you could figure out how to override the default behaviour somehow ([source], [source]), but it's likely that you would end up with several more classes to maintain.
Documentation is clear about that:
You can even have bindable properties on your HTML Only Custom Element. These properties default to one-way databinding, but you can't change the default, though you are still free to explicitly set the binding direction when you bind to the Custom Element.
In my opinion, using .two-way explicit binding is your simplest option here.
<my-element value.two-way="firstName"></my-element>

Vue.js: Trigger an #click on child component from parent component in

I have a template with several divs, each containing an #click that triggers a specific function and a corresponding visual effect
<template>
<div #click="function doThis(param1)></div>
<div #click="function doThis(param2)></div>
<div #click="function doThis(param3)></div>
<div #click="function doThis(param4)></div>
<div #click="function doThis(param5)></div>
</template>
method: {
doThis(param) {
lightUpDiv(corresponding param)
}
}
I'm also calling doThis from the parent via a $broadcast. What if any, is the best Vueish way to trigger the corresponding lightUpDiv call from the $broadcast? (just use getElementById and .click()???). I used individual divs vs a v-for in the hope that this is easier.
I should note that there are several instances of the template On the page. Even if the same visual effect is caused simultaneously on all the templates that is fine. If there is a way to call them on the specific template instances that is even better.
I've spent hours trying to figure this out and cannot seem to do it. Have read all the guide and API. I know there is some way to bind the data to make the template instances unique but can't seem to get it. Definitely couldn't see the #click from $broadcast solution
Thanks for any help!!
<div #click="doThis"></div>
OR
<div #click="doThis($event)"></div>
Both are functionally equivalent, but second version is recommended as it is more explicit.
In javascript,
doThis: function(ev){
//ev is event object and ev.target refers to the div you need.
alert(ev.target.innerHTML);
}
Demo
Event object details

safari - contenteditable, after making it empty, creates an element with text-align:center

In safari,
i had a simple edtable div with a input button, on deletion of the element (backspace or delete), caret moves to center of edtiable div with some inline styled p tag with text-align:center and inline style "color"
<div class="editable" contenteditable="true">
<input type="button" value="inputBtn" />
</div>
http://jsfiddle.net/VqCvt/
its a strange behavior observed only in safari.
Over a year after this post, this issue is still a problem. This issue is directly tied to the input tag. Once an input tag has been in a contenteditable element, Safari will attempt to make the style of the text similar to the input (I confirmed this by observing that the resulting style was different for type="text" vs type="button"). It's a very strange bug. I have found a workaround that works, but it's pretty absurd. My fix is basically to test when my main input no longer has content, and then removing the element, and re-adding it
<div id="content-wrapper">
<div contenteditable="true" id="content" role="textbox"></div>
</div>
and in my "keyup" listener, I put the following code
// Grab main editable content div
var element = document.getElementById("content");
// Check empty state conditions. These work for me, but you may have your own conditions.
if (element.getElementsByTagName("input").length == 0 &&
element.innerText.trim().length == 0) {
// Grab parent container
var elementContainer = document.getElementById("content-wrapper");
// Add a copy of your element to the same specifications. If you have custom style attributes that you set through javascript, don't forget to copy them over
elementContainer.innerHTML = '<div contenteditable="true" id="content" role="textbox"></div>';
// Re-focus the element so the user doesn't have to click again to keep typing
element = document.getElementById("content");
element.focus();
}
What this code does works for my case because input is the only elements which are allowed in my code other than text nodes and <br>, so I first check to make sure there are no input elements, and then make sure the innerText is empty (this indicates no content in my case, you may have to customize your conditions for the "empty" state). Once the empty state is confirmed, I replace the old div with a new one to the same specification, and the user never notices. A very strange issue with a hacky workaround, but I think contenteditables.
You could probably also strip out the HTML that Safari is generating, but for my case this solution is much simpler. I hope this helps someone in the future.