Skip to content

[Vue] Make inputs functional components #19087

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 38 additions & 11 deletions core/src/components/input/usage/vue.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,72 @@
```html
<template>
<!-- Default Input -->
<ion-input></ion-input>
<IonInputVue></IonInputVue>

<!-- Input with value -->
<ion-input value="custom"></ion-input>
<IonInputVue value="custom"></IonInputVue>

<!-- Input with placeholder -->
<ion-input placeholder="Enter Input"></ion-input>
<IonInputVue placeholder="Enter Input"></IonInputVue>

<!-- Input with clear button when there is a value -->
<ion-input clearInput value="clear me"></ion-input>
<IonInputVue clearInput value="clear me"></IonInputVue>

<!-- Number type input -->
<ion-input type="number" value="333"></ion-input>
<IonInputVue type="number" value="333"></IonInputVue>

<!-- Disabled input -->
<ion-input value="Disabled" disabled></ion-input>
<IonInputVue value="Disabled" disabled></IonInputVue>

<!-- Readonly input -->
<ion-input value="Readonly" readonly></ion-input>
<IonInputVue value="Readonly" readonly></IonInputVue>

<!-- Inputs with labels -->
<ion-item>
<ion-label>Default Label</ion-label>
<ion-input></ion-input>
<IonInputVue></IonInputVue>
</ion-item>

<ion-item>
<ion-label position="floating">Floating Label</ion-label>
<ion-input></ion-input>
<IonInputVue></IonInputVue>
</ion-item>

<ion-item>
<ion-label position="fixed">Fixed Label</ion-label>
<ion-input></ion-input>
<IonInputVue></IonInputVue>
</ion-item>

<ion-item>
<ion-label position="stacked">Stacked Label</ion-label>
<ion-input></ion-input>
<IonInputVue></IonInputVue>
</ion-item>

<!-- v-model binding -->
<IonInputVue v-model="foo"></IonInputVue>

<!-- Event listeners -->
<IonInputVue @ionChange="listener"></IonInputVue>

<!-- Call Ionic methods -->
<IonInputVue ref="myInput"></IonInputVue>
</template>

<script>
export default {
data() {
return {
foo: 'default input value',
}
},
methods: {
listener(value) {
console.log(value);
},
focus() {
this.$refs.myInput.setFocus();
}
},
}
</script>
```
114 changes: 69 additions & 45 deletions vue/src/components/inputs.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,81 @@
import Vue from 'vue';
import Vue, { CreateElement, RenderContext } from 'vue';

interface EventHandler {
[key: string]: (e: Event) => void;
}

// Events to register handlers for
const events: string[] = ['ionChange', 'ionInput', 'ionBlur', 'ionFocus', 'ionCancel', 'ionSelect'];

// Arguments to be passed to the factory function
const inputComponentsToBeCreated = [
['IonCheckboxVue', 'ion-checkbox', 'ionChange', 'checked'],
['IonDatetimeVue', 'ion-datetime'],
['IonInputVue', 'ion-input', 'ionInput'],
['IonRadioVue', 'ion-radio', 'ionSelect'],
['IonRangeVue', 'ion-range'],
['IonSearchbarVue', 'ion-searchbar', 'ionInput'],
['IonSelectVue', 'ion-select'],
['IonTextareaVue', 'ion-textarea'],
['IonToggleVue', 'ion-toggle', 'ionChange', 'checked'],
];

// Factory function for creation of input components
export function createInputComponents() {
inputComponentsToBeCreated.map(args => (createInputComponent as any)(...args));
}

/**
* Create a wrapped input component that captures typical ionic input events
* and emits core ones so v-model works.
* @param {} name the vue name of the component
* @param {*} coreTag the actual tag to render (such as ion-datetime)
* @param name the vue name of the component
* @param coreTag the actual tag to render (such as ion-datetime)
* @param modelEvent to be used for v-model
* @param valueProperty to be used for v-model
*/
export function createInputComponent(name: string, coreTag: string, modelEvent = 'ionChange', valueProperty = 'value') {
return Vue.component(name, {
function createInputComponent(name: string, coreTag: string, modelEvent = 'ionChange', valueProperty = 'value') {
Vue.component(name, {
name,
functional: true,
model: {
event: modelEvent,
prop: valueProperty
prop: valueProperty,
},
render(createElement: any) {
// Vue types have a bug accessing member properties:
// https://github.com/vuejs/vue/issues/8721
const cmp: any = this;

return createElement(coreTag, {
'attrs': cmp.attrs,
'on': {
'ionChange': cmp.handleChange,
'ionInput': cmp.handleInput,
'ionBlur': cmp.handleBlur,
'ionFocus': cmp.handleFocus
}
}, this.$slots.default);
render(h: CreateElement, { data, listeners, slots }: RenderContext) {
return h(coreTag, {
...data,
on: buildEventHandlers(listeners, modelEvent, valueProperty),
}, slots().default);
},
methods: {
handleChange($event: any) {
if (modelEvent === 'ionChange') {
// Vue expects the value to be sent as the argument for v-model, not the
// actual event object
this.$emit('ionChange', $event.target[valueProperty]);
} else {
this.$emit('ionChange', $event);
}
},
handleInput($event: any) {
if (modelEvent === 'ionInput') {
// Vue expects the value to be sent as the argument for v-model, not the
// actual event object
this.$emit('ionInput', $event.target[valueProperty]);
} else {
this.$emit('ionInput', $event);
}
},
handleBlur($event: any) {
this.$emit('ionBlur', $event);
},
handleFocus($event: any) {
this.$emit('ionFocus', $event);
}
});
}

function buildEventHandlers(listeners: RenderContext['listeners'], modelEvent: string, valueProperty: string) {
const handlers: EventHandler = {};

// Loop through all the events
events.map((eventName: string) => {
if (!listeners[eventName]) {
return;
}

// Normalize listeners coming from context as Function | Function[]
const callbacks: Function[] = Array.isArray(listeners[eventName])
? listeners[eventName] as Function[]
: [listeners[eventName] as Function];

// Assign handlers
handlers[eventName] = (e: Event) => {
callbacks.map((f: Function) => {
if (e) {
f(modelEvent === eventName
? (e.target as any)[valueProperty]
: e
);
}
});
};
});

return handlers;
}
12 changes: 2 additions & 10 deletions vue/src/ionic.ts
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ import { appInitialize } from './app-initialize';
import { VueDelegate } from './controllers/vue-delegate';
import IonTabs from './components/navigation/ion-tabs';
import IonPage from './components/navigation/ion-page';
import { createInputComponent } from './components/inputs';
import { createInputComponents } from './components/inputs';

export interface Controllers {
actionSheetController: ActionSheetController;
@@ -98,15 +98,7 @@ export const install: PluginFunction<IonicConfig> = (_Vue, config) => {
Vue.component('IonTabs', IonTabs);
Vue.component('IonPage', IonPage);

createInputComponent('IonCheckboxVue', 'ion-checkbox', 'ionChange', 'checked');
createInputComponent('IonDatetimeVue', 'ion-datetime');
createInputComponent('IonInputVue', 'ion-input', 'ionInput');
createInputComponent('IonRadioVue', 'ion-radio');
createInputComponent('IonRangeVue', 'ion-range');
createInputComponent('IonSearchbarVue', 'ion-searchbar', 'ionInput');
createInputComponent('IonSelectVue', 'ion-select');
createInputComponent('IonTextareaVue', 'ion-textarea');
createInputComponent('IonToggleVue', 'ion-toggle', 'ionChange', 'checked');
createInputComponents();

appInitialize(config);