Using jQuery autocomplete or Twitter Typeahead with Aurelia - jquery-ui-autocomplete

I’m trying to add an input filed with jQuery ui autocomplete or Twitter Typeahead. I can’t make either work. I get “$(...).typeahead is not a function” or “$(...).autocomplete is not a function” error.
I also tried aurelia-widget from https://github.com/drivesoftware/aurelia-widgets, but I also get “$(...).autocomplete is not a function” error.
I would appreciate if someone could tell me what I am doing wrong.
locate.js
import {customElement, bindable} from 'aurelia-framework';
import $ from 'jquery';
import { autocomplete } from 'jquery-ui';
#customElement('locate')
export class Locate {
#bindable data;
constructor(element) {
this.element = element;
}
activate() {}
detached(){}
attached(){
$(this.element).autocomplete({
source:['Japan', 'USA', 'Canada', 'Mexico']
});
}
}
locate.html
<template>
<label for="locator-input"></label>
<div id="locator-input-wrapper">
<input id="locator-input" type="text" placeholder="Search">
</div>
</template>

First, you have to be sure about what 'jquery-ui' exports. Does it export something? I believe it exports nothing, instead, it just add some functions to jquery objects. So, you could try this:
import {customElement, bindable} from 'aurelia-framework';
import $ from 'jquery';
import 'jquery-ui';
#customElement('locate')
export class Locate {
#bindable data;
constructor(element) {
this.element = element;
}
activate() {}
detached(){}
attached(){
$(this.element).autocomplete({
source:['Japan', 'USA', 'Canada', 'Mexico']
});
}
}

I had the same error but when I retrieved jquery-ui using npm it worked. So instead of "jspm install jquery-ui" (which gave me the error) try:
jspm install npm:jquery-ui
package.json
"jquery-ui": "npm:jquery-ui#^1.10.5",

I had the same problem with jQuery UI datepicker. So i used jquery-ui-dist instead of jquery-ui when doing NPM install.
import "jquery-ui-dist/jquery-ui";
import "jquery-ui-dist/jquery-ui.min.css";
import "jquery-ui-dist/jquery-ui.theme.min.css";
And then:
$(this.element).datepicker()

There are several step involved on this. Please let me note the key points
First you must install the following packages (I am using nodeJS).
npm install -save jquery jquery-ui
(then and if you are coding on typescript the requested types…)
npm install -save #types/jquery #types/jqueryui
I am installing those packages only for coding with typescript and have intellisense working, but really I will not be using them on runtime.
Where the jquery-ui package resides, on node_modules directory, go and create a ../node_modules/jquery-ui/dist directory.
Then download the built zip minimized version from https://jqueryui.com/ and decompress into that dist directory. Those are the files the we will be really using at runtime.
Configure your AMD loader to point to that dist min file creating paths and shims for jquery and jquery-ui. In my case, the AMD loader is requireJS.
require.config(
{
"paths": {
"jquery": '../node_modules/jquery/dist/jquery.min',
"jquery-ui": '../node_modules/jquery-ui/dist/jquery-ui.min'
(code removed for brevity…)
"shim": {
"jquery": {
"exports": '$'
},
"jquery-ui": {
"exports": '$.autocomplete',
"deps": ['jquery' ]
},
(notice that the line "exports": '$.autocomplete' is not required. Since autocomplete, datepicker, etc. widgets, will be loading onto the $ jQuery global variable, I only used this line only as signaler to my AMD loader the it has really loaded something)
Since my AMD loader don’t “parse” css files, I had to add jquery-ui.min.css style sheet manually to my index.html the
<!DOCTYPE html>
<html>
<head lang="en">
(code removed for brevity…)
<link href="./node_modules/jquery-ui/dist/jquery-ui.min.css" rel="stylesheet" />
Create a custom attribute or a custom element (in my opinion for this case the best choice is a custom attribute
i.e. create a class file called: auto-complete.ts (I am coding on typescript, remove types for vainilla javascript).
import { DOM, inject, bindable, bindingMode } from 'aurelia-framework';
import { fireEvent } 'library';
import * as $ from 'jquery';
import 'jquery-ui';
#inject(DOM.Element)
export class AutoCompleteCustomAttribute {
#bindable source: any;
#bindable options = {};
#bindable({ defaultBindingMode: bindingMode.twoWay }) value: JQueryUI.AutocompleteUIParams;
private readonly element: Element;
constructor(element: Element) {
this.element = element;
}
attached() {
$(this.element).autocomplete({
change: (event, ui) => {
if (ui.item == null) {
$(this.element).val('');
$(this.element).focus();
}
},
select: (label, value) => this.value = value,
source: this.source
}).on('change', e => fireEvent(<any>e.target, 'input'));
}
detached() {
$(this.element).autocomplete('destroy');
}
}
Create a shared module where to code shared functionality (or code directly on custom attribute itself, I am going to stick with the shared module option)
i.e. create a class file called: library.ts
export function fireEvent(element: Element, name: string) {
var event = createEvent(name);
element.dispatchEvent(event);
}
export function createEvent(name: string) {
var event = document.createEvent('Event');
event.initEvent(name, true, true);
return event;
}
The usage of this custom attribute on your code is just to attach it to a input text tag as follows:
<input auto-complete="source.bind:countries; value.two-way: country">
where countries (string array) and country (string) are properties on your view model.
Don’t forget to register your custom attribute as a global resource at your Aurelia project's ./src/resources/index.ts or manually adding it on you main.js configure() function as follows:
aurelia.globalResources(["auto-complete"]);
I hope this answer be usefull
Hi again, I am adding an updated code for the custom attribute here below
import { DOM, inject, bindable, bindingMode } from 'aurelia-framework';
import * as $ from 'jquery';
import 'jquery-ui';
import { fireEvent, AutoCompleteSource } from 'libs/library';
#inject(DOM.Element)
export class AutoCompleteCustomAttribute {
#bindable options = {
applyLabel: true,
forceMatch: true
};
#bindable source: AutoCompleteSource[];
#bindable({ defaultBindingMode: bindingMode.twoWay }) value: number;
#bindable({ defaultBindingMode: bindingMode.twoWay }) label: string;
private readonly element: JQuery<HTMLElement>;
constructor(element: Element) {
this.element = $(element);
}
attached() {
this.element
.autocomplete({
source: this.source,
change: (event, ui) => {
if (ui.item == null && this.options.forceMatch) {
this.element.val('');
}
},
select: (event, ui) => {
if (this.options.applyLabel) {
event.preventDefault();
this.element.val(ui.item.label);
}
this.label = ui.item.label;
this.value = ui.item.value;
},
focus: (event, ui) => {
if (this.options.applyLabel) {
event.preventDefault();
this.element.val(ui.item.label);
}
this.label = ui.item.label;
this.value = ui.item.value;
}
}).on('change', e => fireEvent(<any>e.target, 'input'));
}
detached() {
this.element
.autocomplete('destroy');
}
}
This version funcionality allows us to get the label and the value of the source array when dealing with scenarios where label is the text to search and value is a foreing key.
Added functionality to force the typed text to match one of the existing values.
Added funcionality to apply the label instead of value on the input text display.
Custom attribute should be used as follows:
<input type="text" value="${color}" auto-complete="source.bind:colors;value.bind:colorId;label.bind:color">
where colors (array of { "label": string, "value": number }), colorId (number) and color (string) are properties on your view model.
notice also this new type definition added to the library (just simple typescript stuff)
export type AutoCompleteSource = { "label": string, "value": number };

Related

Is it possible in vue3 to access the root DOM element in a child component slot? I am trying to use a 3rd party library (sortablejs) in vue3

In vue2 I could use this.$el
export default {
render() {
return this.$slots.default[0]
},
mounted() {
Sortable.create(this.$el, {});
})
}
If, in vue3 I try to use this.$slots.default()[0] I can't see how to target the element.
If I use a template ref, I can get the div, but not the contained slot.
The closest question / answer I have found is here Vue 3 Composition API - How to get the component element ($el) on which component is mounted
but this also seems to give the div, but not the slot $el.
This was extremely powerful in vue2 because sortable could be passed a ul, or a div, or another constructed sortable vue component in a slot, and work without the element having to be defined in the child component and I can't work out how to replicate this in vue3.
I originally came across this in a screen cast by Adam Wathan: "Building a Sortable Component with Vue.js", but this was vue2.
I've come up with the following (perhaps there are better out there)
Use template ref:
<template>
<div ref="root">
<slot></slot>
</div>
</template>
Then in the script:
import { ref, onMounted } from 'vue'
export default {
setup() {
const root = ref(null)
onMounted(() => {
// the DOM element will be assigned to the ref after initial render
// console.log(root.value.children[0]) // this is your $el
let el = root.value.children[0]
Sortable.create(el, {})
})
return {
root
}
}
}

Draftail mention plugin for wagtail

I would like to customise the draftail editor in wagtail in order to have "mentions" functionality (I am using the draft js plugin)
Because my react skills are extremely poor and my knowledge of draftail/draftjs very limited, I am using https://github.com/vixdigital/wagtail-plugin-base as a starting point (without knowing much about what it does) and trying to muddle through the task
I have managed to get it so the mention functionality appears in wagtail. The problem is that it appears in a "sub" editor in the toolbar
enter image description here
How can I avoid creating a sub editor and have it work in the body of the existing editor? Below are the two main JS files in my "wagtail-plugin-base"
index.js
import AutoComplete from './AutoComplete/AutoComplete.js';
window.draftail.registerControl(AutoComplete)
AutoComplete.js
import React, { Component } from '../../node_modules/react';
import createMentionPlugin, { defaultSuggestionsFilter } from '../../node_modules/draft-js-mention-plugin';
import editorStyles from './editorStyles.css';
import { mentions } from './mentions'
import { DraftailEditor, BLOCK_TYPE, INLINE_STYLE, createEditorState } from "draftail"
const mentionPlugin = createMentionPlugin();
const AutoComplete = class SimpleMentionEditor extends Component {
state = {
editorState: createEditorState,
suggestions: mentions,
};
onChange = (editorState) => {
this.setState({
editorState,
});
};
onSearchChange = ({ value }) => {
this.setState({
suggestions: defaultSuggestionsFilter(value, mentions),
});
};
onAddMention = () => {
// get the mention object selected
}
focus = () => {
this.editor.focus();
};
render() {
const { MentionSuggestions } = mentionPlugin
return (
<div>
<DraftailEditor
editorState={this.state.editorState}
onChange={this.onChange}
plugins={[mentionPlugin]}
ref={(element) => { this.editor = element; }}
/>
<MentionSuggestions
onSearchChange={this.onSearchChange}
suggestions={this.state.suggestions}
onAddMention={this.onAddMention}
/>
</div>
);
}
}
export default AutoComplete;
Any pointers much appreciated!

Ag-Grid Link with link in the cell

I am building angular 4 app with ag-grid and I am having an issue with trying to figure out how to put a link in the cell. Can anybody help me with that issue?
Thanks
Please check this demo
cellRenderer: function(params) {
return ''+ params.value+''
}
In this demo, the cell value for the column 'city' is a hyperlink.
I struggled with this the other day and it was bit more complex than I first thought. I ended up with creating a renderer component to which I send in the link and that needed a bit on NgZone magic to work all the way. You can use it in your column definition like this:
cellRendererFramework: RouterLinkRendererComponent,
cellRendererParams: {
inRouterLink: '/yourlinkhere',
}
Component where inRouterLink is the link that you send in and params.value is the cell value. That means that you can route to your angular route that could look something like 'yourlink/:id'. You could also simplify this a bit if you don't want a more generic solution by not sending in the link and just hard coding the link in the template and not using the cellRendererParams.
import { Component, NgZone } from '#angular/core';
import { Router } from '#angular/router';
import { AgRendererComponent } from 'ag-grid-angular';
#Component({
template: '<a [routerLink]="[params.inRouterLink,params.value]" (click)="navigate(params.inRouterLink)">{{params.value}}</a>'
})
export class RouterLinkRendererComponent implements AgRendererComponent {
params: any;
constructor(
private ngZone: NgZone,
private router: Router) { }
agInit(params: any): void {
this.params = params;
}
refresh(params: any): boolean {
return false;
}
// This was needed to make the link work correctly
navigate(link) {
this.ngZone.run(() => {
this.router.navigate([link, this.params.value]);
});
}
}
And register it in
#NgModule({
imports: [
AgGridModule.withComponents([
RouterLinkRendererComponent,
])
],
})
UPDATE: I have written a blog post about this: https://medium.com/ag-grid/enhance-your-angular-grid-reports-with-formatted-values-and-links-34fa57ca2952
This is a bit dated, but it may help someone. The solution with typescript on Angular 5 is similar to what C.O.G has suggested.
In the component's typescript file, the column definition can contain a custom cell rendering function.
columnDefs = [
{headerName: 'Client', field: 'clientName' },
{headerName: 'Invoice Number', field: 'invoiceNumber',
cellRenderer: (invNum) =>
`<a href="/invoice/${invNum.value}" >${invNum.value}</a>` },
];
The lambda function is called while rendering the cell. The 'value' of the parameter that gets passed is what you can use to generate custom rendering.
Inspired by #Michael Karén
This is a improved version that is more flexible.
We can set what text to display in link
We can pass more than 2 routerLink parameters
Resolve routerLink according to data
Support target
Display text only if link is not applicable
And more if you wanted to add, just further edit this component
import { Component } from '#angular/core';
import { ICellRendererAngularComp } from 'ag-grid-angular';
export interface IRouterLinkRendererComponentOptions {
routerLinkParams?: any[];
linkDescription?: string;
textOnly?: string;
target?: string;
}
#Component({
template: `
<a *ngIf="params.textOnly == null; else textOnlyBlock"
[routerLink]="params.routerLinkParams"
[target]="params.target ? params.target : '_self'"
>
{{ params.linkDescription }}
</a>
<ng-template #textOnlyBlock>
{{ params.textOnly }}
</ng-template>
`
})
export class RouterLinkRendererComponent implements ICellRendererAngularComp {
params: IRouterLinkRendererComponentOptions;
agInit(params: any): void {
this.params = params.routerLinkRendererComponentOptions(params);
}
refresh(params: any): boolean {
return true;
}
}
So that we can dynamically resolve parameters and return text only if wanted in column definition by
{
...
cellRendererFramework: RouterLinkRendererComponent,
cellRendererParams: {
routerLinkRendererComponentOptions: (param): IRouterLinkRendererComponentOptions => {
if (param.data.dispatch_adjustment) {
return {
routerLinkParams: ['/adjustments', param.data.dispatch_adjustment.id, 'edit'],
linkDescription: '#' + param.data.dispatch_adjustment.id
};
} else {
return {
textOnly: '-'
};
}
}
},
...
},
Instead of using href in cellRenderer , it's better to use cellrenderer framework as route link works in it.
Another Disadvantage is if you use href then the entire angular application will reload again it changes the navigation state from imperative to popstate. The angular router works on the imperative state.
I had implemented something similar to Michael and Tom, with only [routerLink] and no (click) handler. But recently I started getting the dreaded warning:
Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?
After experimenting for awhile I found this post and added the navigate click handler function, which made the application start working again, however I found that the 'Navigation triggered outside Angular zone' message was still appearing in the logs.
So while the (click)="navigate()" call triggers the navigation inside the ngZone,the [routerLink] call is still being made, which bothered me. I really didn't want two attempts to navigate to happen - in case anything changed with a future API update.
I decided to replace the anchor tag with a span pseudoLink.
.pseudoLink {
color: blue;
text-decoration: underline;
cursor: pointer;
}
#Component({
template: '<span class="pseudoLink" (click)="navigate()">{{mytitle}}</span>'
})
navigate() {
this.ngZone.run(
() => {
console.log("LinkRendererComponent: navigate: (", this.mylink, ")");
this.router.navigate([this.mylink]);
}
);
}
this.mylink is defined in the agInit() method based on parameters passed in via cellRendererParams.
This works well for my main purpose which is to make the cell look like a link. Only thing I lost was the URL path popup in the browser status bar.
Hope this might help someone else.
Using a cell renderer is the correct solution but missing from the top answer is stopping the click event from reaching AgGrid:
cellRenderer: ({value}) => {
const a = document.createElement('a');
a.innerText = a.href = value;
a.target = '_blank';
// Prevent click from reaching AgGrid
a.addEventListener('click', event => { event.stopPropagation() });
return a;
}
If the click bubbles up to AgGrid it will cause row selection changes, etc if those are enabled.
I created a generic component that is usable for any link cell, uses no workarounds, and logs no warnings.
Usage
columnDefs = [
{
colId: 'My Column',
cellRendererFramework: AgGridLinkCellComponent,
cellRendererParams: {
// `text` and `link` both accept either an string expression (same as `field`) or a function that gets ICellRendererParams
text: 'title',
link: (params: ICellRendererParams) => `/my-path/${_.get(params, 'data.id')}`
}
}
]
Register the component in your AppModule:
imports: [
AgGridModule.withComponents([
AgGridLinkCellComponent
])
]
The component itself:
import * as _ from 'lodash';
import {Component} from '#angular/core';
import {AgRendererComponent} from 'ag-grid-angular';
import {ICellRendererParams} from 'ag-grid-community';
#Component({
selector: 'app-ag-grid-link-cell-component',
template: '<a [routerLink]="link">{{ text }}</a>',
})
export class AgGridLinkCellComponent implements AgRendererComponent {
link: string;
text: string;
constructor() {
}
agInit(params: ICellRendererParams): void {
this.refresh(params);
}
refresh(params: ICellRendererParams): boolean {
const dataParams = params.colDef.cellRendererParams;
this.link = _.isFunction(dataParams.link) ? dataParams.link(params) : _.get(params.data, dataParams.link);
this.text = _.isFunction(dataParams.text) ? dataParams.link(params) : _.get(params.data, dataParams.text);
return false;
}
}
We had this problem, and its not straightforward.
We ended up solving it in a different way as we use AdapTable on top of ag-Grid.
So we created an AdapTable Action Column and in the RenderFunction provided the link. That worked best for us as we didnt always want the Link to appear so we could use the ShouldRender function to decide whether or not we wanted to display link for each row.

Aurelia Webpack datepicker is not a function

I have a custom attribute "datepicker" that complains of
TypeError: $(...).datepicker is not a function
I am using skeleton-typescript-webpack as my starting point.
datepicker.ts
import {inject, autoinject, customAttribute, DOM} from "aurelia-framework";
import moment from "moment";
import "jquery";
import "jquery-ui";
#customAttribute("datepicker")
#inject(DOM.Element)
export class JqueryUiDatePicker {
constructor(private element: Element) {
moment().format();
}
attached() {
$(this.element)
.datepicker({dateFormat: "DD/MM/YYYY"})
.on("change", e => this.fireEvent((e.target) as Element, "input"));
}
detached() {
$(this.element)
.datepicker("destroy")
.off("change");
}
private fireEvent(element: Element, name: string): void {
const event = this.createEvent(name);
this.element.dispatchEvent(event);
}
private createEvent(name: string): Event {
const event = document.createEvent("Event");
event.initEvent(name, true, true);
return event;
}
}
markup
<input class="form-control" id="orderDate" datepicker placeholder="Order Date" type="text" value.bind="model.orderDate | dateFormat: 'DD/MM/YYYY'">
It used to work when Aurelia was in beta, but after I've updated the my code with the latest skeleton-typescript-webpack, it stopped working.
Try import $ from 'jquery';.
Here's blog for a related example: http://davismj.me/blog/semantic-custom-element/
I am using au cli (with typescript)... so YMMV; but the following import worked for me
import * as $ from 'jquery'
yes, you do need to import jquery-ui as well

Create a polymer element using an ES6 class?

I'd like to be able to create a class to pass to the Polymer function to create elements. The obvious use case is to have a base class that our developers can use to build Polymer elements.
However, Polymer seems to ignore lifecycle methods on the class. The following code does not run #created. Is there a workaround?
//attempt 1
class CustomElement {
is = 'sample-multi-view-polymer-buic'
created() { console.log('created') } // never called
}
export default Polymer(new CustomElement()) // doesn't work (see error below)
Since I'm using Babel to transpile ES6 to ES5, the above is equivalent to the code below. It also does not work.
// attempt2
function CustomElement() {
this.is = 'sample-multi-view-polymer-buic'
}
CustomElement.prototype.created= function() { console.log('hi') } //never called
There's an article for using ES6 with Polymer in the docs
So there's 2 things missing on your end:
You are missing a beforeRegistered() method.
You need to use Polymer() to instantiate your element's Class.
Here's how I do it:
<base href="https://polygit.org/components/">
<script src="webcomponentsjs/webcomponents-lite.min.js"></script>
<link href="polymer/polymer.html" rel="import">
<link href="paper-input/paper-input.html" rel="import">
<dom-module id="x-example">
<template>
<paper-input value="{{name::input}}"></paper-input>
<h4>{{name}}</h4>
</template>
<script>
"use strict";
class xExample {
beforeRegister() {
this.is = "x-example";
this.properties = {
name: {
type: String,
value: "I'm a data-binded prop"
}
}
}
ready() {
console.log("ready");
}
}
Polymer(xExample);
</script>
</dom-module>
<x-example></x-example>
Note:
The above snippet only works on Chrome/FF
For everything else you can of course "babelize" it.