Dojo 2 How to achieve two-way binding for input? - dojo2

How to achieve 2 way data binding for input in dojo 2?
handleChange = (e) => {
this.setState({ textValue: e.target.value });}
<Input name='title' defaultValue={this.state.textValue} placeholder='title...' onChange={this.handleChange} />
I know this is how we do in React but don't know how to achieve in dojo 2.

In fact React supports only one-way binding, and your example illustrates it well. You need to update state, to re-render react component.
And as far as I understood from dojo2 docs and tutorials, there is almost same approach under the hood. Take a look here
Dojo 2 is built around unidirectional, top-down property propagation where it is the parent widget’s job to pass properties down to its children. In fact, a child widget has no direct reference to a parent widget! When a property changes, widgets are re-rendered (using an efficient virtual DOM) to reflect the updated state.
And it may look like this:
private _addWorker() {
this._workerData = this._workerData.concat(this._newWorker);
this._newWorker = {};
this.invalidate();
}
You change data and call invalidate() to re-render widget.

This is the solution to achieve 2 way data binding in Dojo 2.
InputWidget:-
interface IInputProps {
value: string;
onChange(event: Event): void;
}
export class InputWidget extends WidgetBase<IInputProps> {
private _onChange (event: Event) {
event.stopPropagation();
this.properties.onChange && this.properties.onChange((event.target as HTMLInputElement).value);
}
protected render(): DNode {
const { value } = this.properties;
return v('input', {
key: "input",
type: "text",
value
onchange: this._onChange
});
}
}
InputWrapper widget:-
export class InputWrapper extends WidgetBase<IWrapperProps> {
private inputValue: string = '';
protected inputValueChanges(value: string) {
this.inputValue = value;
this.invalidate();
}
protected render(): DNode {
<div>
{w(InputWidget, {onchange: this.inputValueChanges, value: this.inputValue })}
<span>Input Value:- {this.inputValue}</span>
</div>
}
}
This is the solution to achieve 2 way data binding in Dojo 2.
Hope this will be helpful! :(

Related

How to convert Vue2 code to pure web components

I want to implement the autocomplete search (the one on the left) from this codepen to pure web components. But something went wrong because slots don't work and something else also doesn't work but I can't figure out what it is. What I have so far
Search-select
const template = `
<p>
<slot name="autocomp" results="${this.results}"
searchList="${(event) => this.setQuery(event)}"
>
fgfgfg
</slot>
yo
</p>
`;
class SearchSelect extends HTMLElement {
constructor() {
super();
this.query = "";
this.results = [];
this.options = [
"Inside Out",
"John Wick",
"Jurassic World",
"The Lord of the Rings",
"Pacific Rim",
"Pirates of the Caribbean",
"Planet of the Apes",
"Saw",
"Sicario",
"Zombies",
];
this.shadow = this.attachShadow({ mode: "open" });
}
setQuery(event) {
console.log(event.target);
this.query = event.target.value;
}
get options() {
return this.getAttribute("options");
}
set options(val) {
this.setAttribute("options", val);
}
static get observedAttributes() {
return ["options", "filterMethod"];
}
filterMethod(options, query) {
return options.filter((option) =>
option.toLowerCase().includes(query.toLowerCase())
);
}
attributeChangedCallback(prop, oldValue, newValue) {
if (prop === "options") {
this.results = this.filterMethod(this.options, this.query);
this.render();
}
if (prop === "filterMethod") {
this.results = this.filterMethod(this.options, this.query);
this.render();
}
}
render() {
this.shadow.innerHTML = template;
}
connectedCallback() {
this.render();
}
}
customElements.define("search-select", SearchSelect);
Autocomplete
const templ = `
<search-select>
<div class="autocomplete">
<input
type="text"
placeholder="Type to search list"
onchange="${this.searchList}"
onfocus="${this.showDropdown}"
onblur="${this.hideDropdown}"
/>
<div class="autocomplete-dropdown" v-if="dropdownVisible">
<ul class="autocomplete-search-results-list">
${this.result}
</ul>
</div>
</div>
</search-select>
`;
class Autocomplete extends HTMLElement {
constructor() {
super();
this.dropdownVisible = false;
this.rslts = "";
this.shadow = this.attachShadow({ mode: "open" });
}
get results() {
return this.getAttribute("results");
}
set results(val) {
this.setAttribute("results", val);
}
get searchList() {
return this.getAttribute("searchList");
}
showDropdown() {
this.dropdownVisible = true;
}
hideDropdown() {
this.dropdownVisible = false;
}
attributeChangedCallback(prop, oldValue, newValue) {
this.render();
}
render() {
this.shadow.innerHTML = templ;
}
connectedCallback() {
this.render();
}
}
customElements.define("auto-complete", Autocomplete);
Your current approach is completely wrong. Vue is reactive framework. Web components do not provide reactivity out of box.
The translation of Vue2 component to direct Web component is not straight forward. The slots do not work because Vue.js slots are not the same as Web component slots. They are just conceptually modeled after them.
First, when you use the Vue.js slot, you are practically putting some part of the vDOM (produced as a result of JSX) defined by the calling component into the Search or Autocomplete component. It is not a real DOM. Web components, on the other hand, provide slot which actually accepts a real DOM (light DOM).
Next, your render method is practically useless. You are simply doing this.shadow.innerHTML = template; which will simply append the string as HTML into the real DOM. You are not resolving the template nodes. Vue.js provides a reactivity out of box (that's why you need Vue/React). Web components do not provide such reactivity. On each render, you are re-creating entire DOM which is not a good way to do it. When you are not using any framework to build web component, you should construct all the required DOM in connectedCallback and then keep on selectively updating using DOM manipulation API. This is imperative approach to building UIs.
Third, you are using named slot while consuming it in auto complete, you are not specifying the named slot. So whatever is the HTML you see is not getting attached to the Shadow DOM.
You will need to
Building a complex component like Auto Complete needs a basic reactivity system in place that takes care of efficiently and automatically updating the DOM. If you do not need full framework, consider using Stencil, LitElement, etc. If you can use Vue.js, just use it and wrap it into Web component using helper function.
For Vue 2, you can use the wrapper helper library. For Vue 3, you can use the built-in helper.

Handling input changes in mobx

Let's say that I have two forms, each related to a seperate mobx store. One form is for Client info (first name, last name etc), and the other for Employee info. Each form obviously has multiple inputs that update the observables in the related store.
In this example I have an action in each store that takes an event and based on the name, updates the value:
#action handleInputChange = (e) => {
this[e.target.name] = e.target.value
}
Is there a way to abstract this action into a helper file, something that would contain common actions, instead of retyping this again and again?
Thanks in advance, I'm pretty new to this as you can imagine.
There are several ways to handle the question. In my project, I just wrote an HOC(Higher-Order Component) to do that.
export default function asForm(MyComponent, formDataProp) {
return #observer class Form extends Component {
// constructor, etc.
updateProperty(key, value) {
this.props[formDataProp][key] = value;
}
// some other functions like double click prevention, etc.
render() {
return (
<MyComponent
{...this.props}
updateProperty={this.updateProperty}
// some other props
/>
);
}
};
}
Then use the HOC like this:
#observer
class UserForm extends Component {
render() {
const { updateProperty, userInfo } = this.props;
return (
<div className="form-wrapper">
<YourInputComponent
name="name"
updateProperty={updateProperty}
value={userInfo.name}
// other props
/>
</div>
);
}
}
UserForm.propTypes = {
userInfo: PropTypes.instanceOf(UserInfo),
updateProperty: PropTypes.func.isRequired,
};
export default asForm(UserForm, 'userInfo');
I am not sure if this solution violates the rule that you should not assign values to props.

mobx, render when a property value changes

I'm just getting started with Mobx in a react-native project and am having trouble understanding how to perform changes on a observed object.
Changing the object reference via the setWorkingObject action function in my store properly renders the UI, however if I just want to change a single property within this object, how do I cause a render?
My "store":
export default class MyStore {
constructor() {
extendObservable(this, {
workingObject: null
});
}
}
My "container":
class Container extends Component {
render() {
return (
<Provider store={new MyStore()}>
<App />
</Provider>
);
}
}
and my "component", which uses a simple custom input component (think of it like Checkbox) to perform changes to a property of my workingObject
class MyClass extends Component {
...
render() {
const {store} = this.props;
return
<View>
...
<RadioGroup
options={[
{ title: "One", value: 1 },
{ title: "Two", value: 2 }
]}
onPress={option => {
store.workingObject.numberProperty = option.value;
}}
selectedValue={store.workingObject.numberProperty}
/>
...
</View>
}
}
export default inject("store")(observer(MyClass));
I can't figure out why this doesn't work, in fact it looks very similar to the approach used in this example
Any other tips/critique on how I've implemented mobx welcome
The problem is that only existing properties are made observable at the time the workingObject is first assigned.
The solution is to declare future properties at the time of assignment, ie:
// some time prior to render
store.workingObject = { numberProperty:undefined };
First, you don't want to set initial value to null. Second, adding properties to observable object after it was created will not make added properties observable. You need to use extendObservable() instead of assigning new properties directly to observable object. Another solution is to use observable map instead.
in your store:
extendObservable(this, {
workingObject: {}
});
in your component:
extendObservable(store.workingObject, {numberProperty: option.value});
I recommend using Map in this case:
extendObservable(this, {workingObject: new Map()});
in your component:
store.workingObject.set(numberProperty, option.value);

Technique for jquery change events and aurelia

I need to find a reliable solution to making the two frameworks play nicely.
Using materialize-css, their select element uses jquery to apply the value change. However that then does not trigger aurelia in seeing the change. Using the technique of...
$("select")
.change((eventObject: JQueryEventObject) => {
fireEvent(eventObject.target, "change");
});
I can fire an event aurelia sees, however, aurelia then cause the event to be triggered again while it's updating it's bindings and I end up in an infinite loop.... Stack Overflow :D
Whats the most reliable way of getting the two to play together in this respect?
I have worked with materialize-css + aurelia for a while and I can confirm that the select element from materialize is quite problematic.
I just wanted to share one of my solutions here in case anyone wants some additional examples. Ashley's is probably cleaner in this case. Mine uses a bindable for the options instead of a slot.
Other than that the basic idea is the same (using a guard variable and a micro task).
One lesson I learned in dealing with 3rd party plugins and two-way data binding is that it helps to make a more clear, distinct separation between handling changes that originate from the binding target (the select element on the DOM) and changes that originate from the binding source (e.g. the ViewModel of the page containing the element).
I tend to use change handlers with names like onValueChangedByBindingSource and onValueChangedByBindingTarget to deal with the different ways of syncing the ViewModel with the DOM in a way that results in less confusing code.
Example: https://gist.run?id=6ee17e333cd89dc17ac62355a4b31ea9
src/material-select.html
<template>
<div class="input-field">
<select value.two-way="value" id="material-select">
<option repeat.for="option of options" model.bind="option">
${option.displayName}
</option>
</select>
</div>
</template>
src/material-select.ts
import {
customElement,
bindable,
bindingMode,
TaskQueue,
Disposable,
BindingEngine,
inject,
DOM
} from "aurelia-framework";
#customElement("material-select")
#inject(DOM.Element, TaskQueue, BindingEngine)
export class MaterialSelect {
public element: HTMLElement;
public selectElement: HTMLSelectElement;
#bindable({ defaultBindingMode: bindingMode.twoWay })
public value: { name: string, value: number };
#bindable({ defaultBindingMode: bindingMode.oneWay })
public options: { displayName: string }[];
constructor(
element: Element,
private tq: TaskQueue,
private bindingEngine: BindingEngine
) {
this.element = element;
}
private subscription: Disposable;
public isAttached: boolean = false;
public attached(): void {
this.selectElement = <HTMLSelectElement>this.element.querySelector("select");
this.isAttached = true;
$(this.selectElement).material_select();
$(this.selectElement).on("change", this.handleChangeFromNativeSelect);
this.subscription = this.bindingEngine.collectionObserver(this.options).subscribe(() => {
$(this.selectElement).material_select();
});
}
public detached(): void {
this.isAttached = false;
$(this.selectElement).off("change", this.handleChangeFromNativeSelect);
$(this.selectElement).material_select("destroy");
this.subscription.dispose();
}
private valueChanged(newValue, oldValue): void {
this.tq.queueMicroTask(() => {
this.handleChangeFromViewModel(newValue);
});
}
private _suspendUpdate = false;
private handleChangeFromNativeSelect = () => {
if (!this._suspendUpdate) {
this._suspendUpdate = true;
let event = new CustomEvent("change", {
bubbles: true
});
this.selectElement.dispatchEvent(event)
this._suspendUpdate = false;
}
}
private handleChangeFromViewModel = (newValue) => {
if (!this._suspendUpdate) {
$(this.selectElement).material_select();
}
}
}
EDIT
How about a custom attribute?
Gist: https://gist.run?id=b895966489502cc4927570c0beed3123
src/app.html
<template>
<div class="container">
<div class="row"></div>
<div class="row">
<div class="col s12">
<div class="input-element" style="position: relative;">
<select md-select value.two-way="currentOption">
<option repeat.for="option of options" model.bind="option">${option.displayName}</option>
</select>
<label>Selected: ${currentOption.displayName}</label>
</div>
</div>
</div>
</div>
</template>
src/app.ts
export class App {
public value: string;
public options: {displayName: string}[];
constructor() {
this.options = new Array<any>();
this.options.push({ displayName: "Option 1" });
this.options.push({ displayName: "Option 2" });
this.options.push({ displayName: "Option 3" });
this.options.push({ displayName: "Option 4" });
}
public attached(): void {
this.value = this.options[1];
}
}
src/md-select.ts
import {
customAttribute,
bindable,
bindingMode,
TaskQueue,
Disposable,
BindingEngine,
DOM,
inject
} from "aurelia-framework";
#inject(DOM.Element, TaskQueue, BindingEngine)
#customAttribute("md-select")
export class MdSelect {
public selectElement: HTMLSelectElement;
#bindable({ defaultBindingMode: bindingMode.twoWay })
public value;
constructor(element: Element, private tq: TaskQueue) {
this.selectElement = element;
}
public attached(): void {
$(this.selectElement).material_select();
$(this.selectElement).on("change", this.handleChangeFromNativeSelect);
}
public detached(): void {
$(this.selectElement).off("change", this.handleChangeFromNativeSelect);
$(this.selectElement).material_select("destroy");
}
private valueChanged(newValue, oldValue): void {
this.tq.queueMicroTask(() => {
this.handleChangeFromViewModel(newValue);
});
}
private _suspendUpdate = false;
private handleChangeFromNativeSelect = () => {
if (!this._suspendUpdate) {
this._suspendUpdate = true;
const event = new CustomEvent("change", { bubbles: true });
this.selectElement.dispatchEvent(event)
this.tq.queueMicroTask(() => this._suspendUpdate = false);
}
}
private handleChangeFromViewModel = (newValue) => {
if (!this._suspendUpdate) {
$(this.selectElement).material_select();
}
}
}
Ok, I spent entirely too long getting this one answered the way I wanted, but more on that later. The actual answer to stop the infinite loop is fairly simple, so let's look at it first. You need to have a guard property, and you'll need to use Aurelia's TaskQueue to help unset the guard property.
Your code will look a little something like this:
$(this.selectElement).change(evt => {
if(!this.guard) {
this.guard = true;
const changeEvent = new Event('change');
this.selectElement.dispatchEvent(changeEvent);
this.taskQueue.queueMicroTask(() => this.guard = false);
}
});
Notice that I'm using queueing up a microtask to unset the guard. This makes sure that everything will work the way you want.
Now that we've got that out of the way, let's look at a gist I created here. In this gist I created a custom element to wrap the Materialize select functionality. While creating this, I learned that select elements and content projection via slot elements don't go together. So you'll see in the code that I have to do some coding gymnastics to move the option elements over from a dummy div element into the select element. I'm going to file an issue so we can look in to this and see if this is a bug in the framework or simply a limitation of the browser.
Normally, I would highly recommend creating a custom element to wrap this functionality. Given the code I had to write to shuffle nodes around, I can't say that I highly recommend creating a custom element. I just really recommend it in this case.
But anyways, there you go!

Aurelia `click` attribute that requires event target to be same as element

I'm aware of click.trigger as well as click.delegate which work fine. But what if I want to assign a click event that should only trigger when the exact element that has the attribute gets clicked?
I'd probably do something like this were it "normal" JS:
el.addEventListener('click', function (e) {
if (e.target === el) {
// continue...
}
else {
// el wasn't clicked directly
}
});
Is there already such an attribute, or do I need to create one myself? And if so, I'd like it to be similar to the others, something like click.target="someMethod()". How can I accomplish this?
Edit: I've tried this which doesn't work because the callback function's this points to the custom attribute class - not the element using the attribute's class;
import { inject } from 'aurelia-framework';
#inject(Element)
export class ClickTargetCustomAttribute {
constructor (element) {
this.element = element;
this.handleClick = e => {
console.log('Handling click...');
if (e.target === this.element && typeof this.value === 'function') {
console.log('Target and el are same and value is function :D');
this.value(e);
}
else {
console.log('Target and el are NOT same :/');
}
};
}
attached () {
this.element.addEventListener('click', this.handleClick);
}
detached () {
this.element.removeEventListener('click', this.handleClick);
}
}
And I'm using it like this:
<div click-target.bind="toggleOpen">
....other stuff...
</div>
(Inside this template's viewModel the toggleOpen() method's this is ClickTargetCustomAttribute when invoked from the custom attribute...)
I'd also prefer if click-target.bind="functionName" could instead be click.target="functionName()" just like the native ones are.
Just use smth like click.delegate="toggleOpen($event)".
$event is triggered event, so you can handle it in toggleOpen
toggleOpen(event) {
// check event.target here
}
Also you can pass any other value available in template context to toggleOpen.