Draftail mention plugin for wagtail - plugins

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!

Related

Can you manipulate the DOM directly while using Preactjs?

I am looking into Preact for my next project.
Since it has no virtual DOM I am wondering if it, like React, prefers you to let the framework manipulate the DOM instead of doing so yourself directly.
Would Preact bump heads with another library that manipulates the DOM such as SVGjs?
Preact is non-destructive when it comes to DOM updates. The official guide already explains how to integrate external DOM manipulations into the preact component:
If using class-based component:
import { h, Component } from 'preact';
class Example extends Component {
shouldComponentUpdate() {
// IMPORTANT: do not re-render via diff:
return false;
}
componentWillReceiveProps(nextProps) {
// you can do something with incoming props here if you need
}
componentDidMount() {
// now mounted, can freely modify the DOM:
const thing = document.createElement('maybe-a-custom-element');
this.base.appendChild(thing);
}
componentWillUnmount() {
// component is about to be removed from the DOM, perform any cleanup.
}
render() {
return <div class="example" />;
}
}
If using hooks, then use memo function from preact/compat:
import { h } from 'preact';
import { useEffect } from 'preact/hooks';
import { memo } from 'preact/compat';
function Example(props) {
const [node, setNode] = setState(null);
useEffect(() => {
const elm = document.createElement('maybe-a-custom-element');
setNode(elm);
// Now do anything with the elm.
// Append to body or <div class="example"></div>
}, []);
return <div class="example" />;
}
// Usage with default comparison function
const Memoed = memo(Example);
// Usage with custom comparison function
const Memoed2 = memo(Example, (prevProps, nextProps) => {
// Only re-render when `name' changes
return prevProps.name === nextProps.name;
});
Also, note that Preact's render() function always diffs DOM children inside of the container. So if your container contains DOM that was not rendered by Preact, Preact will try to diff it with the elements you pass it. - Thus the meaning non-destructive.

How to use "template" and "rowTemplate" field in syncfusion React DataGrid?

I am setting up a Data Grid table from React syncfusion in my application.
My application is built with antDesign Pro template, DvaJS, UmiJS, and ReactJS.
I have made the basic syncfusion Data Grid that uses default fields to fetch the cell data and it works fine.
As soon as I add "template" field to ColumnDirective or "rowTemplate" to GridComponent, I get the error.
import React, { Component } from 'react';
import router from 'umi/router';
import { connect } from 'dva';
import { Input } from 'antd';
import moment from 'react-moment';
import { ColumnDirective, ColumnsDirective, Filter, Grid, GridComponent } from '#syncfusion/ej2-react-grids';
#connect(({loading, data})=> ({
data: data.data,
loading: loading.effects['data/fetchData']
}))
class dataComponent extends Component {
constructor(){
super(...arguments);
this.state = {
data: [],
}
this.columnTemplate = this.columnTemplate;
}
columnTemplate(props) {
return (
<div className="image"><p>text</p></div>
);
}
render() {
const { match, children, location, dispatch, data} = this.props;
return (
<GridComponent dataSource={data}>
<ColumnsDirective>
<ColumnDirective headerText='Heading' template={this.columnTemplate}/>
<ColumnDirective field='EmployeeID' headerText='Employee ID'/>
<ColumnDirective field='FirstName' headerText='Name'/>
</ColumnsDirective>
</GridComponent>
);
}
Expected output:
Heading Employee ID FirstName
Text 123 John
In actual, it doesn't render anything after adding template field to code.
In console, I get this error:
Uncaught TypeError: str.match is not a function
at evalExp (template.js:65)
at compile (template.js:52)
at Object.push../node_modules/#syncfusion/ej2-grids/node_modules/#syncfusion/ej2-base/src/template-engine.js.Engine.compile (template-engine.js:57)
at compile (template-engine.js:16)
at templateCompiler (util.js:145)
at new Column (column.js:131)
at prepareColumns (util.js:185)
at GridComponent.push../node_modules/#syncfusion/ej2-grids/src/grid/base/grid.js.Grid.render (grid.js:704)
at GridComponent.push../node_modules/#syncfusion/ej2-react-grids/src/grid/grid.component.js.GridComponent.render (grid.component.js:35)
at finishClassComponent (react-dom.development.js:14741)
at updateClassComponent (react-dom.development.js:14696)
at beginWork (react-dom.development.js:15644)
at performUnitOfWork (react-dom.development.js:19312)
at workLoop (react-dom.development.js:19352)
at HTMLUnknownElement.callCallback (react-dom.development.js:149)
at Object.invokeGuardedCallbackDev (react-dom.development.js:199)
at invokeGuardedCallback (react-dom.development.js:256)
at replayUnitOfWork (react-dom.development.js:18578)
at renderRoot (react-dom.development.js:19468)
at performWorkOnRoot (react-dom.development.js:20342)
at performWork (react-dom.development.js:20254)
at performSyncWork (react-dom.development.js:20228)
at requestWork (react-dom.development.js:20097)
at scheduleWork (react-dom.development.js:19911)
at Object.enqueueSetState (react-dom.development.js:11169)
at DynamicComponent../node_modules/react/cjs/react.development.js.Component.setState (react.development.js:335)
at dynamic.js:91
When I click on template.js:65
it show the following as error:
var isClass = str.match(/class="([^\"]+|)\s{2}/g);
Here's the link to code I am trying to follow:
Syncfusion template example
I'd highly appreciate your help!
Query #1: How to use the column template on Syncfusion EJ2 Grid in React platform.
You can customize the own element instead of field value in EJ2 Grid column. Please refer the below code example, sample and Help Documentation for more information.
export class ColumnTemplate extends SampleBase {
constructor() {
super(...arguments);
this.template = this.gridTemplate;
}
gridTemplate(props) {
return (
<div className="image"><p>text</p></div> //Here defined your element
);
}
render() {
return (<div className='control-pane'>
<div className='control-section'>
<GridComponent dataSource={employeeData} width='auto' height='359'>
<ColumnsDirective>
<ColumnDirective headerText='Heading' width='180' template={this.template} textAlign='Center'/>
<ColumnDirective field='EmployeeID' headerText='Employee ID' width='125' textAlign='Right'/>
<ColumnDirective field='FirstName' headerText='Name' width='120'/>
</ColumnsDirective>
</GridComponent>
</div>
</div>);
}
}
Sample link: https://stackblitz.com/edit/react-gdraob?file=index.js
Help Documentation: https://ej2.syncfusion.com/react/documentation/grid/columns/#column-template
Query #2: How to use the row template on Syncfusion EJ2 Grid in React platform.
We have provided the EJ2 Grid with row template feature support it is allow template for the row and we have rendered element instead of each Grid row. Please refer the below code example, sample and Help Documentation for more information.
export class RowTemplate extends SampleBase {
constructor() {
super(...arguments);
this.format = (value) => {
return instance.formatDate(value, { skeleton: 'yMd', type: 'date' });
};
this.template = this.gridTemplate;
}
gridTemplate(props) {
return (<tr className="templateRow">
<td className="details">
<table className="CardTable" cellPadding={3} cellSpacing={2}>
. . . .
</tbody>
</table>
</td>
</tr>);
}
render() {
return (<div className='control-pane'>
<div className='control-section'>
<GridComponent dataSource={employeeData} rowTemplate={this.template.bind(this)} width='auto' height='335'>
<ColumnsDirective>
<ColumnDirective headerText='Employee Details' width='300' textAlign='Left' />
</ColumnsDirective>
</GridComponent>
</div>
</div>);
}
}
Sample link: https://stackblitz.com/edit/react-ka9ixk?file=index.js
Please get back to us, if you need further assistance.
Regards,
Thavasianand S.

Angular2 NgModel not getting value in Jasmine test

I am using template-driven forms in Angular 2, and I'm trying to develop them test-first. I've scoured this site and the rest of the internet and I've tried basically everything I can find (mainly bunches of tick() statements and detectChanges() everywhere in a fakeAsync) to get the NgModel attached to my input to pick up the value so it can be passed to my onSubmit function. The value of the input element sets properly, but the NgModel never updates, which then means the onSubmit function does not get the correct value from the NgModel.
Here's the template:
<form id="createWorkout" #cwf="ngForm" (ngSubmit)="showWorkout(skillCountFld)" novalidate>
<input name="skillCount" id="skillCount" class="form-control" #skillCountFld="ngModel" ngModel />
<button type="submit" id="buildWorkout">Build a Workout</button>
</form>
Note: I know that the value sent the ngSubmit is going to cause the test to fail, but it means I can set a break point in the function and inspect the NgModel.
Here's the Component:
import { Component, OnInit } from '#angular/core';
import {SkillService} from "../model/skill-service";
import {NgModel} from "#angular/forms";
#Component({
selector: 'app-startworkout',
templateUrl: './startworkout.component.html',
styleUrls: ['./startworkout.component.css']
})
export class StartworkoutComponent implements OnInit {
public skillCount:String;
constructor(public skillService:SkillService) { }
showWorkout(value:NgModel):void {
console.log('breakpoint', value.value);
}
ngOnInit() {
}
}
Here is the spec:
/* tslint:disable:no-unused-variable */
import {async, ComponentFixture, TestBed, fakeAsync, tick} from '#angular/core/testing';
import {By, BrowserModule} from '#angular/platform-browser';
import { DebugElement } from '#angular/core';
import { StartworkoutComponent } from './startworkout.component';
import {SkillService} from "../model/skill-service";
import {Store} from "../core/store";
import {SportService} from "../model/sport-service";
import {FormsModule} from "#angular/forms";
import {dispatchEvent} from "#angular/platform-browser/testing/browser_util";
describe('StartworkoutComponent', () => {
let component: StartworkoutComponent;
let fixture: ComponentFixture;
let element:DebugElement;
let skillService:SkillService;
beforeEach(async(() => {
var storeSpy:any = jasmine.createSpyObj('store', ['getValue', 'storeValue', 'removeValue']);
var stubSkillService:SkillService = new SkillService(storeSpy);
TestBed.configureTestingModule({
declarations: [ StartworkoutComponent ],
providers: [{provide:Store , useValue:storeSpy}, SportService, SkillService],
imports: [BrowserModule, FormsModule]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(StartworkoutComponent);
component = fixture.componentInstance;
element = fixture.debugElement;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('without workout', () => {
let createWorkout:DebugElement;
let skillCount:HTMLInputElement;
let submitButton:HTMLButtonElement;
beforeEach(() => {
createWorkout = element.query(By.css('#createWorkout'));
skillCount = element.query(By.css('#skillCount')).nativeElement;
submitButton = element.query(By.css('#buildWorkout')).nativeElement;
});
it('has createWorkout form', () => {
expect(createWorkout).toBeTruthy();
expect(skillCount).toBeTruthy();
});
it('submits the value', fakeAsync(() => {
spyOn(component, 'showWorkout').and.callThrough();
tick();
skillCount.value = '10';
dispatchEvent(skillCount, 'input');
fixture.detectChanges();
tick(50);
submitButton.click();
fixture.detectChanges();
tick(50);
expect(component.showWorkout).toHaveBeenCalledWith('10');
}));
});
});
I'm sure I'm missing something basic/simple, but I've spent the past day combing through everything I can find with no luck.
Edit:
I think maybe people are focusing on the wrong thing. I'm pretty sure at this point that I'm missing something basic about how ngForm and ngModel work. When I add
<p>{{cwf.value | json}}</p>
into the form, it just shows {}. I believe it should show a member property representing the input. If I type into the field, the value does not change. Similar things happen if I try to bind to skillCountFld. So I think the basic form setup is incorrect somehow, and the test is never going to work until the input is correctly wired to the skillCountFld controller. I just don't see what I'm missing.
There are a lot of tests at the Angular site that are successfully
setting this without waiting for whenStable https://github.com/angular/angular/blob/874243279d5fd2bef567a13e0cef8d0cdf68eec1/modules/%40angular/forms/test/template_integration_spec.ts#L1043
That's because all code in those tests is executed inside fakeAsync zone while you are firing fixture.detectChanges(); within beforeEach. So fakeAsync zone doesn't know about async operation outside its scope. When you're calling detectChanges first time ngModel is initialized
NgModel.prototype.ngOnChanges = function (changes) {
this._checkForErrors();
if (!this._registered)
this._setUpControl(); //<== here
and gets right callback for input event
NgForm.prototype.addControl = function (dir) {
var _this = this;
resolvedPromise.then(function () { // notice async operation
var container = _this._findContainer(dir.path);
dir._control = (container.registerControl(dir.name, dir.control));
setUpControl(dir.control, dir); // <== here
inside setUpControl you can see function that will be called by input event
dir.valueAccessor.registerOnChange(function (newValue) {
dir.viewToModelUpdate(newValue);
control.markAsDirty();
control.setValue(newValue, { emitModelToViewChange: false });
});
1) So if you move fixture.detectChanges from beforeEach to your test then it should work:
it('submits the value', fakeAsync(() => {
spyOn(component, 'showWorkout').and.callThrough();
fixture.detectChanges();
skillCount = element.query(By.css('#skillCount')).nativeElement;
submitButton = element.query(By.css('#buildWorkout')).nativeElement;
tick();
skillCount.value = '10';
dispatchEvent(skillCount, 'input');
fixture.detectChanges();
submitButton.click();
fixture.detectChanges();
expect(component.showWorkout).toHaveBeenCalledWith('10');
}));
Plunker Example
But this solution seems very complicated since you need to rewrite your code to move fixture.detectChanges in each of your it statements (and there is also a problem with skillCount, submitButton etc)
2) As Dinistro said async together with whenStable should also help you:
it('submits the value', async(() => {
spyOn(component, 'showWorkout').and.callThrough();
fixture.whenStable().then(() => {
skillCount.value = '10';
dispatchEvent(skillCount, 'input');
fixture.detectChanges();
submitButton.click();
fixture.detectChanges();
expect(component.showWorkout).toHaveBeenCalledWith('10');
})
}));
Plunker Example
but wait why do we have to change our code?
3) Just add async to your beforeEach function
beforeEach(async(() => {
fixture = TestBed.createComponent(StartworkoutComponent);
component = fixture.componentInstance;
element = fixture.debugElement;
fixture.detectChanges();
}));
Plunker Example
I think whenStable should do the trick in your case:
it('submits the value',() => {
fixture.whenStable().then(() => {
// ngModel should be available here
})
});
More information here: https://angular.io/docs/ts/latest/guide/testing.html#!#when-stable
EDIT:
The ValueAccessor seems not to notice the change if you directly manipulate the value of the DOM-element. But it should notice, if you write the value directly with the ValueAccessor:
const skillCount= fixture.debugElement.query(By.directive(NgModel));
const ngModel= skillCount.injector.get(NgModel);
ngModel.valueAccessor.writeValue('10')

Popup always open in the marker

Is there any way the popup always stays open and does not need to click on it to open?
Expected behaviour
Actual behavior
With the introduction of react-leaflet version 2 which brings breaking changes in regard of creating custom components, it is no longer supported to extend components via inheritance (refer this thread for a more details)
In fact React official documentation also recommends to use composition instead of inheritance:
At Facebook, we use React in thousands of components, and we haven’t
found any use cases where we would recommend creating component
inheritance hierarchies.
Props and composition give you all the flexibility you need to
customize a component’s look and behavior in an explicit and safe way.
Remember that components may accept arbitrary props, including
primitive values, React elements, or functions.
The following example demonstrates how to extend marker component in order to keep popup open once the marker is displayed:
const MyMarker = props => {
const initMarker = ref => {
if (ref) {
ref.leafletElement.openPopup()
}
}
return <Marker ref={initMarker} {...props}/>
}
Explanation:
get access to native leaflet marker object (leafletElement) and open popup via Marker.openPopup method
Here is a demo
What you can do is to make your own Marker class from the react-leaflet marker, and then call the leaflet function openPopup() on the leaflet object after it has been mounted.
// Create your own class, extending from the Marker class.
class ExtendedMarker extends Marker {
componentDidMount() {
// Call the Marker class componentDidMount (to make sure everything behaves as normal)
super.componentDidMount();
// Access the marker element and open the popup.
this.leafletElement.openPopup();
}
}
This will make the popup open once the component has been mounted, and will also behave like a normal popup afterwards, ie. on close/open.
I threw together this fiddle that shows the same code together with the basic example.
You can use permanent tooltips, or React provides refs for this type of thing... you can do this:
https://jsfiddle.net/jrcoq72t/121/
const React = window.React
const { Map, TileLayer, Marker, Popup } = window.ReactLeaflet
class SimpleExample extends React.Component {
constructor () {
super()
this.state = {
lat: 51.505,
lng: -0.09,
zoom: 13
}
}
openPopup (marker) {
if (marker && marker.leafletElement) {
window.setTimeout(() => {
marker.leafletElement.openPopup()
})
}
}
render () {
const position = [this.state.lat, this.state.lng]
return (
<Map center={position} zoom={this.state.zoom}>
<TileLayer attribution='© OpenStreetMap contributors' url="http://{s}.tile.osm.org/{z}/{x}/{y}.png" />
<Marker position={position} ref={this.openPopup}>
<Popup>
<span>
A pretty CSS3 popup. <br /> Easily customizable.
</span>
</Popup>
</Marker>
</Map>
)
}
}
window.ReactDOM.render(<SimpleExample />, document.getElementById('container'))
References:
https://reactjs.org/docs/refs-and-the-dom.html
React.js - access to component methods
Auto open markers popup on react-leaflet map
The above no longer works with react-leaflet version 3. In your custom marker component, to get a reference to the leaflet element you should now use useRef() and then open up the popup in useEffect() once the component is mounted.
const MyMarker = (props) => {
const leafletRef = useRef();
useEffect(() => {
leafletRef.current.openPopup();
},[])
return <Marker ref={leafletRef} {...props} />
}
For the new react-leaflet v4 you will need to do some changes
const CustomMarker = ({ isActive, data, map }) => {
const [refReady, setRefReady] = useState(false);
let popupRef = useRef();
useEffect(() => {
if (refReady && isActive) {
map.openPopup(popupRef);
}
}, [isActive, refReady, map]);
return (
<Marker position={data.position}>
<Popup
ref={(r) => {
popupRef = r;
setRefReady(true);
}}
>
{data.title}
</Popup>
</Marker>
);
};
And then use MapContainer like this
const MapComponent = () => {
const [map, setMap] = useState(null);
return (
<div>
<MapContainer
ref={setMap}
center={[45.34416, 15.49005]}
zoom={15}
scrollWheelZoom={true}
>
<CustomMarker
isActive
map={map}
data={{
position: [45.34416, 15.49005],
title: "Text displayed in popup",
}}
/>
</MapContainer>
</div>
);
};
Edited
Notice that my old solution would try to open the popup every time it renders.
Found another solution that fit my needs to open it when the position changed. Notice that I look at position.lat, position.lng since it will think it always changes if you pass on the object.
And yes it is not perfect typescript but it is the best solution I could come up with.
const CustomMarker: React.FC<CustomMarkerProps> = ({ position, children }) => {
const map = useMap();
const markerRef = useRef(null);
useEffect(() => {
try {
// #ts-ignore
if (markerRef.current !== null && !markerRef.current.isPopupOpen()) {
// #ts-ignore
markerRef.current.openPopup();
}
} catch (error) {}
}, [position.lat, position.lng]);
return (
<Marker ref={markerRef} position={position}>
<Popup>{children}</Popup>
</Marker>
);
};
export default CustomMarker;
Old solution
Could not get it to work using useRef and useEffect. However, got it to work with calling openPopup() directly from the ref.
import { LatLngLiteral, Marker as LMarker } from "leaflet";
import React from "react";
import { Marker } from "react-leaflet";
export interface CustomMarkerProps {
position: LatLngLiteral;
open?: boolean;
}
const CustomMarker: React.FC<CustomMarkerProps> = ({
position,
open = false,
children,
}) => {
const initMarker = (ref: LMarker<any> | null) => {
if (ref && open) {
ref.openPopup();
}
};
return (
<Marker ref={initMarker} position={position}>
{children}
</Marker>
);
};
export default CustomMarker;

Using jQuery autocomplete or Twitter Typeahead with Aurelia

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 };