Aurelia js providing dynamic content to popover body - aurelia

I am following the structure to implement tool tip from Sean Hunter Blog . Now i want provide tool-tip content as a dynamic html content i.e I want to show one html pattern inside content. How can I provide using Aurelia framework. In knockout JS using custom binding handler I am providing content as with id of division like below code.
Knockout Structure is
<button data-bind="tooltip: { template: 'ElementId', trigger: 'click', placement: 'right', container: 'body' }">Click Me</button>
<div id="ElementId" style="display: none;">
<div>Dynamic content will go here</div>
</div>
How to achieve same with Aurelia Structure:
<template>
<require from="templates/popover/tooltip"></require>
<button data-toggle="tooltip" tooltip="placement:bottom;trigger:click;html:true;template:'ElementId';title:tooltip Header">Click Me</button>
<div id="ElementId" style="display: none;">
<div>Dynamic content will go here</div>
</div>
</template>
Custom Attribute code
import {customAttribute, inject, bindable} from 'aurelia-framework';
import $ from 'bootstrap';
#customAttribute('tooltip')
#inject(Element)
export class Tooltip {
element: HTMLElement;
#bindable title: any;
#bindable placement: any;
#bindable content: any;
#bindable template: any;
constructor(element) {
this.element = element;
}
bind() {
if (this.content) {
$(this.element).tooltip({ title: this.title, placement: this.placement, content: this.content });
}
else {
$(this.element).tooltip({ title: this.title, placement: this.placement, template: $('#'+this.template).html() });
}
}
// gets fired when the provided value changes, although not needed in this example since the json from reddit is static
titleChanged(newValue) {
$(this.element).data('bs.tooltip').options.title = newValue;
}
contentChanged(newValue) {
if (this.content) {
$(this.element).data('bs.tooltip').options.content = newValue;
}
else {
$(this.element).data('bs.tooltip').options.template = newValue;
}
}
placementChanged(newValue) {
$(this.element).data('bs.tooltip').options.placement = newValue;
}
}

You would need to implement the rest of bootstrap's popover API in your custom attribute, and add some logic to turn a selector into a template.
Here's an example: https://gist.run?id=909c7aa984477a465510abe2fd25c8a1
Note: i've added the default values from bootstrap popovers for clarity
With a custom attribute:
src/app.html
<template>
<h1>${message}</h1>
<button class="btn btn-block btn-default" popover="title.bind: message; placement: top">Default popover</button>
<button class="btn btn-block btn-default" popover="title.bind: message; template-selector: #popoverTemplate; placement: bottom; html: true">Custom popover</button>
<div id="popoverTemplate">
<div class="popover" role="tooltip">
<div class="arrow"></div>
<h3 class="popover-title"></h3>
<div>Some custom html</div>
</div>
</div>
</template>
src/app.ts
export class App {
message = "Hello world";
}
src/popover.ts
import {inject, customAttribute, bindable, DOM} from "aurelia-framework";
#customAttribute("popover")
#inject(DOM.Element)
export class Popover {
public element: HTMLElement;
constructor(element) {
this.element = element;
}
#bindable({defaultValue: true})
public animation: boolean;
#bindable({defaultValue: false})
public container: (string | false);
#bindable({defaultValue: 0})
public delay: (number | object);
#bindable({defaultValue: false})
public html: boolean;
#bindable({defaultValue: "right"})
public placement: (string | Function);
#bindable({defaultValue: false})
public selector: (string | false);
#bindable({defaultValue: `<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>`})
public template: string;
#bindable({defaultValue: false})
public templateSelector: (string | false);
#bindable({defaultValue: ""})
public title: (string | Function);
#bindable({defaultValue: "click"})
public trigger: string;
#bindable({defaultValue: { selector: "body", padding: 0 }})
public viewport: (string | Object | Function);
public attached(): void {
let template;
if (this.templateSelector) {
const templateElement = document.querySelector(this.templateSelector);
template = templateElement.innerHTML;
} else {
template = this.template;
}
$(this.element).popover({
animation: this.animation,
container: this.container,
delay: this.delay,
html: this.html,
placement: this.placement,
selector: this.selector,
template: template,
title: this.title,
trigger: this.trigger,
viewport: this.viewport
});
}
}
With a custom element:
This is in response to #Ashley Grant's comment. It could improve clarity if you used a custom element for this. I'm not sure of the implementation he had in mind, but this would be one way to make it work without really losing flexibility.
src/app.html
<template>
<h1>${message}</h1>
<popover-element title.bind="message" placement="bottom">
</popover-element>
<popover-element title.bind="message" placement="bottom">
<button slot="popoverTarget" class="btn btn-block btn-default">
Custom popover (custom element)
</button>
<div slot="popoverTemplate" class="popover" role="tooltip">
<div class="arrow"></div>
<h3 class="popover-title"></h3>
<div>Some custom html</div>
<div>Message: ${message}</div>
</div>
</popover-element>
</template>
src/app.ts
export class App {
message = "Hello world";
}
src/popover-element.html
<template>
<div ref="target">
<slot name="popoverTarget">
<button class="btn btn-block btn-default">Default popover (custom element)</button>
</slot>
</div>
<div ref="template">
<slot name="popoverTemplate">
<div class="popover" role="tooltip">
<div class="arrow"></div>
<h3 class="popover-title"></h3>
<div class="popover-content"></div>
</div>
</slot>
</div>
</template>
src/popover-element.ts
import {customElement, bindable} from "aurelia-framework";
#customElement("popover-element")
export class PopoverElement {
public template: HTMLElement;
public target: HTMLElement;
#bindable({defaultValue: true})
public animation: boolean;
#bindable({defaultValue: false})
public container: (string | false);
#bindable({defaultValue: 0})
public delay: (number | object);
#bindable({defaultValue: false})
public html: boolean;
#bindable({defaultValue: "right"})
public placement: (string | Function);
#bindable({defaultValue: false})
public selector: (string | false);
#bindable({defaultValue: ""})
public title: (string | Function);
#bindable({defaultValue: "click"})
public trigger: string;
#bindable({defaultValue: { selector: "body", padding: 0 }})
public viewport: (string | Object | Function);
public attached(): void {
$(this.target.firstElementChild).popover({
animation: this.animation,
container: this.container,
delay: this.delay,
html: this.html,
placement: this.placement,
selector: this.selector,
template: this.template.firstElementChild.outerHTML,
title: this.title,
trigger: this.trigger,
viewport: this.viewport
});
}
}

You can remove the '.outerHTML' from this line template: this.template.firstElementChild.outerHTML, as template: this.template.firstElementChild, in order to get the model binding.

Related

Bootstrap Popover Content not Reactive Vue2

I am trying to create a component for a popover using Bootstrap4 in Vue:
<template>
<div>
<div :id="uid + '_Content'" class="d-none">
<slot name="content">
</slot>
</div>
<div :class="'call' + uid">
<slot name="caller">
</slot>
</div>
</div>
</template>
<script>
module.exports = {
data() {
return {
uid: this.genUID()
}
},
props: {
title: {
type: String,
required: false,
default: ''
}
},
mounted() {
_this = this;
// PopOver Definition
const popover = new bootstrap.Popover(document.querySelector('.call' + this.uid), {
container: 'body',
title: this.title,
html: true,
placement: 'auto',
sanitize: false,
customClass: 'noselect',
content: () => {
return document.querySelector("#" + _this.uid + "_Content").innerHTML;
}
});
},
methods: {
genUID() {
return "Popover_" + Math.random().toString(16).slice(2);
}
}
}
</script>
However, when passing content to <slot name="content"> from another component, the data is not reactive. Is there any config information that I am missing to make it reactive? Is this even possible with Vue and (regular) Bootstrap (not Bootstrap-Vue).
You're losing reactivity because your content option to bootstrap.Popover is returning a string of your element's HTML, not the element itself. The popover just copies the HTML as it exists when it is opened. If you pass the element, Bootstrap will reparent the element itself into the popover, so changes to the element's children should be reflected. (Note that this could still be disrupted by a virtual DOM change that rewrote the element itself, which is why Bootstrap-Vue would still be better here.) If the popover might be reused, you'll need to reparent the element back into your component's own tree each time the popover is closed. You'll also need to make provision for the _Content element to only be hidden while it isn't reparented.
Here's how it all would look:
<template>
<div ref="container" class="Popover__Container">
<div ref="content" class="Popover__Content">
<slot name="content">
</slot>
</div>
<div ref="caller" class="Popover__Caller">
<slot name="caller">
</slot>
</div>
</div>
</template>
<script>
module.exports = {
props: {
title: {
type: String,
required: false,
default: ''
}
},
mounted() {
const content = this.$refs.content;
// PopOver Definition
const popover = new bootstrap.Popover(this.$refs.caller, {
container: 'body',
title: this.title,
html: true,
placement: 'auto',
sanitize: false,
customClass: 'noselect',
content: content
});
$(this.$refs.caller).on('hidden.bs.popover', () =>
{
this.$refs.container.prepend(content);
});
}
}
</script>
<style scoped>
.Popover__Container > .Popover__Content {
display: none;
}
</style>
(I've also replaced the UID approach with refs, since that is a more Vue-like approach.)

Vuejs v-on click doesn't work inside component

I use VueJs and I create the following component with it.
var ComponentTest = {
props: ['list', 'symbole'],
data: function(){
return {
regexSymbole: new RegExp(this.symbole),
}
},
template: `
<div>
<ul>
<li v-for="item in list"
v-html="replaceSymbole(item.name)">
</li>
</ul>
</div>
`,
methods: {
replaceSymbole: function(name){
return name.replace(this.regexSymbole, '<span v-on:click="test">---</span>');
},
test: function(event){
console.log('Test ...');
console.log(this.$el);
},
}
};
var app = new Vue({
el: '#app',
components: {
'component-test': ComponentTest,
},
data: {
list: [{"id":1,"name":"# name1"},{"id":2,"name":"# name2"},{"id":3,"name":"# name3"}],
symbole: '#'
},
});
and this my html code
<div id="app">
<component-test :list="list" :symbole="symbole"></component-test>
</div>
When I click on the "span" tag inside "li" tag, nothing append.
I don't have any warnings and any errors.
How I can call my component method "test" when I click in the "span" tag.
How implement click event for this case.
You cannot use vue directives in strings that you feed to v-html. They are not interpreted, and instead end up as actual attributes. You have several options:
Prepare your data better, so you can use normal templates. You would, for example, prepare your data as an object: { linkText: '---', position: 'before', name: 'name1' }, then render it based on position. I think this is by far the nicest solution.
<template>
<div>
<ul>
<li v-for="(item, index) in preparedList" :key="index">
<template v-if="item.position === 'before'">
<span v-on:click="test">{{ item.linkText }}</span>
{{ item.name }}
</template>
<template v-else-if="item.position === 'after'">
{{ item.name }}
<span v-on:click="test">{{ item.linkText }}</span>
</template>
</li>
</ul>
</div>
</template>
<script>
export default {
props: ["list", "symbole"],
computed: {
preparedList() {
return this.list.map(item => this.replaceSymbole(item.name));
}
},
methods: {
replaceSymbole: function(question) {
if (question.indexOf("#") === 0) {
return {
linkText: "---",
position: "before",
name: question.replace("#", "").trim()
};
} else {
return {
linkText: "---",
position: "after",
name: question.replace("#", "").trim()
};
}
},
test: function(event) {
console.log("Test ...");
console.log(this.$el);
}
}
};
</script>
You can put the click handler on the surrounding li, and filter the event. The first argument to your click handler is the MouseEvent that was fired.
<template>
<div>
<ul>
<li v-for="item in list" :key="item.id" v-on:click="clickHandler"
v-html="replaceSymbole(item.name)">
</li>
</ul>
</div>
</template>
<script>
export default {
props: ["list", "symbole"],
data() {
return {
regexSymbole: new RegExp(this.symbole)
};
},
computed: {
preparedList() {
return this.list.map(item => this.replaceSymbole(item.name));
}
},
methods: {
replaceSymbole: function(name) {
return name.replace(
this.regexSymbole,
'<span class="clickable-area">---</span>'
);
},
test: function(event) {
console.log("Test ...");
console.log(this.$el);
},
clickHandler(event) {
const classes = event.srcElement.className.split(" ");
// Not something you do not want to trigger the event on
if (classes.indexOf("clickable-area") === -1) {
return;
}
// Here we can call test
this.test(event);
}
}
};
</script>
Your last option is to manually add event handlers to your spans. I do not!!! recommend this. You must also remove these event handlers when you destroy the component or when the list changes, or you will create a memory leak.

input not beign validated by form

I created an input component to reuse it between a few forms. In one of then, it's working perfectly, but in the other, it's not.
It doesn't throw any erros. I even receive the input value after submit.
code.component.html
<div [ngClass]="aplicaCssErro(ag)">
<label for="code">Code</label>
<input id="code" name="code" type="text" class="form-control" [(ngModel)]="value" required #ag="ngModel"
maxlength="4" minlength="4" (blur)="formatCode(ag)">
<div *ngIf="verificaValidTouched(ag)" class="msgErroText">
<gce-campo-control-erro [mostrarErro]="ag?.errors?.required" msgErro="the code is required">
</gce-campo-control-erro>
</div>
code.component.ts
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: 'gce-input-code',
templateUrl: './input-code.component.html',
styleUrls: ['./input-code.component.scss']
})
export class InputCodeComponent implements OnInit {
#Input() value: string = "";
constructor() { }
ngOnInit() {
}
//some functions
}
form.component.html
The problem is that the form is not validating it, just the first input.
I think the form is not recognizing it as one of it's inputs.
<form (ngSubmit)="onSubmitForm2(f)" #f="ngForm">
<div class="row">
<div class="col-sm-6" [ngClass]="aplicaCssErro(apelido)">
<label for="apelido">Apelido da conta</label>
<input id="apelido" name="apelido" type="text" class="form-control" alt="Apelido" [(ngModel)]="conta.apelido" required #apelido="ngModel">
<div *ngIf="verificaValidTouched(apelido)" class="msgErroText">
<gce-campo-control-erro [mostrarErro]="apelido?.errors?.required" msgErro="O Apelido é obrigatório.">
</gce-campo-control-erro>
</div>
</div>
</div>
<div class="row">
<div class="form-group">
<div class="col-sm-2">
<gce-input-code name="code" [(ngModel)]="user.code" #code="ngModel" ngDefaultControl></gce-input-code>
</div>
</div>
</div>
<div class="row">
<button class="btn btn-default" name="btn2" type="submit" alt="Continuar" [disabled]="!f.valid">Continue</button>
</div>
Any help?
If I understand your question correctly. You are trying to make it so the form(ngForm) can validate the custom component that wraps around the input(gce-input-code).
A normal form does not have any way to know what is going in/out of the component as it is Angular component. You would have to enhance your code.component.ts to include all the connectors (ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS) into it.
Checkout this blog
https://blog.thoughtram.io/angular/2016/07/27/custom-form-controls-in-angular-2.html#custom-form-control-considerations
and its plnkr(exerpt code below)
https://plnkr.co/edit/6xVdppNQoLcsXGMf7tph?p=info
import { Component, OnInit, forwardRef, Input, OnChanges } from '#angular/core';
import { FormControl, ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS }
from '#angular/forms';
export function createCounterRangeValidator(maxValue, minValue) {
return (c: FormControl) => {
let err = {
rangeError: {
given: c.value,
max: maxValue || 10,
min: minValue || 0
}
};
return (c.value > +maxValue || c.value < +minValue) ? err: null;
}
}
#Component({
selector: 'counter-input',
template: `
<button (click)="increase()">+</button> {{counterValue}} <button (click)="decrease()">-</button>
`,
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CounterInputComponent), multi: true },
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => CounterInputComponent), multi: true }
]
})
export class CounterInputComponent implements ControlValueAccessor, OnChanges {
propagateChange:any = () => {};
validateFn:any = () => {};
#Input('counterValue') _counterValue = 0;
#Input() counterRangeMax;
#Input() counterRangeMin;
get counterValue() {
return this._counterValue;
}
set counterValue(val) {
this._counterValue = val;
this.propagateChange(val);
}
ngOnChanges(inputs) {
if (inputs.counterRangeMax || inputs.counterRangeMin) {
this.validateFn = createCounterRangeValidator(this.counterRangeMax, this.counterRangeMin);
this.propagateChange(this.counterValue);
}
}
writeValue(value) {
if (value) {
this.counterValue = value;
}
}
registerOnChange(fn) {
this.propagateChange = fn;
}
registerOnTouched() {}
increase() {
this.counterValue++;
}
decrease() {
this.counterValue--;
}
validate(c: FormControl) {
return this.validateFn(c);
}
}

Vue-Dropzone processQueue not working

On my website you can upload a dog with attributes and images.
Vuejs is the frontend and Laravel the backend.
I am using this vue-dropzone component in my project to upload images.
The problem
I want to upload the images and the attributes of a dog at the same time (when the user clicks the submit button), so that the image files can be linked to the dog's id in the database.
Laravel function to register a new dog (route: 'api/dogs')
public function store(Request $request)
{
$attributes = [
'name' => $request->input('name'),
'type' => $request->input('dogType'),
...
];
$dogId = Dog::insertGetId($attributes);
// Upload files
if ($request->hasFile('files')) {
// getting all files
$files = $request->file('files');
// Count files to be uploaded
$file_count = count($files);
// start count how many uploaded
$uploadcount = 0;
if($uploadcount == $file_count) {
return true;
} else {
FileController::store($request, 0, 0, $dogId, $files, $uploadcount);
}
}
return $dogId;
}
Dropzone component (Formdropzone)
<template>
<div>
<dropzone
:id="this.id"
:url="this.url"
:accepted-file-types='"image/*"'
:use-font-awesome="true"
:preview-template="template"
:auto-process-queue="false" <----
:upload-multiple="true"
:parallel-uploads=100
:max-files=100
#vdropzone-success="showSuccess"
>
</dropzone>
</div>
</template>
<script>
import Dropzone from 'vue2-dropzone'
export default {
props: {
id: {
type: String,
required: true
},
url: {
type: String,
required: true
}
},
components: {
Dropzone
},
methods: {
showSuccess(file) {
console.log('A file was successfully uploaded')
},
template() {
return `
<div class="dz-preview dz-file-preview">
<div class="dz-image" style="width: 200px;height: 200px">
<img data-dz-thumbnail /></div>
<div class="dz-details">
<div class="dz-size"><span data-dz-size></span></div>
<div class="dz-filename"><span data-dz-name></span></div>
</div>
<div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
<div class="dz-error-message"><span data-dz-errormessage></span></div>
<div class="dz-success-mark"><i class="fa fa-check"></i></div>
<div class="dz-error-mark"><i class="fa fa-close"></i></div>
</div>
`;
}
}
}
</script>
Register dog component
<tab-content title="Images">
<div class="form__input__wrapper">
<span class="label">Images (optional)</span>
<formdropzone url="http://domain.local/api/dogs" ref="dogDropzone" id="dogDropzone"></formdropzone>
</div>
</tab-content>
<script>
import Formdropzone from './Formdropzone'
export default {
data() {
return {
dog:{
name: '',
dogType: '',
...
}
}
},
methods: {
publish() {
this.$http.post('api/dogs', this.dog)
.then(response => {
this.$refs.dogDropzone.processQueue() <----
this.$router.push('/feed')
})
}
},
components: {
'formdropzone': Formdropzone
}
</script>
The error message
Uncaught (in promise) TypeError: Cannot read property 'processQueue' of undefined
I would be very thankful for any kind of help!

Vue view update?

Build A Single page app with vue 2 and vue-router 2
build.vue:
<style>
#build-content {
margin: 20px 20px;
}
</style>
<template>
<div id="build-content">
<h2>title</h2>
<div v-for="(buildValue, buildKey) in currentConfig">
<li v-for="(value, key) in buildValue"
is="build-item"
v-bind:buildEventId="buildKey"
v-bind:buildKey="key"
v-bind:buildValue="value"
v-on:remove="remove">
</li>
</div>
<br>
<br>
</div>
</template>
<script>
import BuildItem from './build-item.vue'
import Vue from "vue";
import qs from 'qs';
export default {
components:{ BuildItem },
data () {
return {
currentConfig: {
"1" : {
"akey" : "aValue",
"bkey" : "bValue",
"ckey" : "cValue",
},
"2" : {
"akey" : "aValue",
"bkey" : "bValue",
"ckey" : "cValue",
}
}
}
},
methods: {
remove: function (eventId, key) {
console.log(eventId + " " + key);
Vue.delete(this.currentConfig[eventId], key);
}
},
mounted: function () {
}
}
</script>
build-item.vue:
<style scoped>
.tab {
margin-right:2em
}
</style>
<template>
<div>
<br>
<span class="tab">event</span>
<Input v-model="eventId" placeholder="input..." style="width: 150px" class="tab"/>
<span class="tab">key:</span>
<Input v-model="key" placeholder="input..." style="width: 200px" class="tab"/>
<span class="tab">value:</span>
<Input v-model="value" placeholder="input..." style="width: 300px" class="tab"/>
<Button type="error" #click="remove">remove</Button>
</div>
</template>
<script>
export default {
data () {
return {
eventId: this.buildEventId,
key: this.buildKey,
value: this.buildValue,
}
},
props: {
buildEventId: {
type: String
},
buildKey: {
type: String
},
buildValue:{
type: String
}
},
methods: {
remove: function () {
this.$emit('remove', this.eventId, this.buildKey);
}
}
}
</script>
Click the first row of the list("1","akey","aValue'),but remove the third row("1","cKey","cValue") ,console.log output is correct,how to fix it?
Thanks
https://v2.vuejs.org/v2/guide/list.html#key
This default mode is efficient, but only suitable when your list
render output does not rely on child component state or temporary DOM
state (e.g. form input values).
<div v-for="(buildValue, buildKey) in currentConfig" :key="buildKey">
<li v-for="(value, key) in buildValue" :key="key"
is="build-item"
v-bind:buildEventId="buildKey"
v-bind:buildKey="key"
v-bind:buildValue="value"
v-on:remove="remove">
</li>
</div>
add :key="buildKey" and :key="key" ,Fixed the problem