Skip to content

Commit 5d514fe

Browse files
Adds support for events on schemas, resolves wearebraid#132
1 parent 4e3723e commit 5d514fe

File tree

7 files changed

+109
-16
lines changed

7 files changed

+109
-16
lines changed

dist/formulate.esm.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/formulate.min.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/formulate.umd.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/FormulateForm.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<FormulateSchema
88
v-if="schema"
99
:schema="schema"
10+
v-on="schemaListeners"
1011
/>
1112
<FormulateErrors
1213
v-if="!hasFormErrorObservers"
@@ -83,6 +84,10 @@ export default {
8384
},
8485
computed: {
8586
...useRegistryComputed(),
87+
schemaListeners () {
88+
const { submit, ...listeners } = this.$listeners
89+
return listeners
90+
},
8691
pseudoProps () {
8792
return extractAttributes(this.$attrs, classProps.filter(p => /^form/.test(p)))
8893
},

src/FormulateSchema.js

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
1-
import { cyrb43 } from './libs/utils'
1+
import { cyrb43, has } from './libs/utils'
22

33
/**
44
* Given an object and an index, complete an object for schema-generation.
55
* @param {object} item
66
* @param {int} index
77
*/
8-
export function leaf (item, index = 0) {
8+
export function leaf (item, index = 0, rootListeners = {}) {
99
if (item && typeof item === 'object' && !Array.isArray(item)) {
1010
let { children = null, component = 'FormulateInput', depth = 1, key = null, ...attrs } = item
1111
// these next two lines are required since `class` is a keyword and should
1212
// not be used in rest/spread operators.
1313
const cls = attrs.class || {}
1414
delete attrs.class
15+
// Event bindings
16+
const on = {}
17+
18+
// Extract events from this instance
19+
const events = Object.keys(attrs)
20+
.reduce((events, key) => /^@/.test(key) ? Object.assign(events, { [key.substr(1)]: attrs[key] }) : events, {})
21+
22+
// delete all events from the item
23+
Object.keys(events).forEach(event => {
24+
delete attrs[`@${event}`]
25+
on[event] = createListener(event, events[event], rootListeners)
26+
})
1527

1628
const type = component === 'FormulateInput' ? (attrs.type || 'text') : component
1729
const name = attrs.name || type || 'el'
@@ -31,7 +43,7 @@ export function leaf (item, index = 0) {
3143
const els = Array.isArray(children)
3244
? children.map(child => Object.assign(child, { depth: depth + 1 }))
3345
: children
34-
return Object.assign({ key, depth, attrs, component, class: cls }, els ? { children: els } : {})
46+
return Object.assign({ key, depth, attrs, component, class: cls, on }, els ? { children: els } : {})
3547
}
3648
return null
3749
}
@@ -41,21 +53,44 @@ export function leaf (item, index = 0) {
4153
* @param {Functon} h createElement
4254
* @param {Array|string} schema
4355
*/
44-
function tree (h, schema) {
56+
function tree (h, schema, rootListeners) {
4557
if (Array.isArray(schema)) {
4658
return schema.map((el, index) => {
47-
const item = leaf(el, index)
59+
const item = leaf(el, index, rootListeners)
4860
return h(
4961
item.component,
50-
{ attrs: item.attrs, class: item.class, key: item.key },
51-
item.children ? tree(h, item.children) : null
62+
{ attrs: item.attrs, class: item.class, key: item.key, on: item.on },
63+
item.children ? tree(h, item.children, rootListeners) : null
5264
)
5365
})
5466
}
5567
return schema
5668
}
5769

70+
/**
71+
* Given an event name and handler, return a handler function that re-emits.
72+
*
73+
* @param {string} event
74+
* @param {string|boolean|function} handler
75+
*/
76+
function createListener (eventName, handler, rootListeners) {
77+
return function (...args) {
78+
// For event leafs like { '@blur': function () { ..do things... } }
79+
if (typeof handler === 'function') {
80+
return handler.call(this, ...args)
81+
}
82+
// For event leafs like { '@blur': 'nameBlur' }
83+
if (typeof handler === 'string' && has(rootListeners, handler)) {
84+
return rootListeners[handler].call(this, ...args)
85+
}
86+
// For event leafs like { '@blur': true }
87+
if (has(rootListeners, eventName)) {
88+
return rootListeners[eventName].call(this, ...args)
89+
}
90+
}
91+
}
92+
5893
export default {
5994
functional: true,
60-
render: (h, { props }) => tree(h, props.schema)
95+
render: (h, { props, listeners }) => tree(h, props.schema, listeners)
6196
}

src/libs/registry.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -300,11 +300,12 @@ export function useRegistryMethods (without = []) {
300300
const keys = Array.from(new Set(Object.keys(values).concat(Object.keys(this.proxy))))
301301
keys.forEach(field => {
302302
const input = this.registry.has(field) && this.registry.get(field)
303-
if (input && !equals(input.proxy, values[field], true)) {
304-
input.context.model = values[field]
303+
let value = values[field]
304+
if (input && !equals(input.proxy, value, true)) {
305+
input.context.model = value
305306
}
306-
if (!equals(values[field], this.proxy[field], true)) {
307-
this.setFieldValue(field, values[field])
307+
if (!equals(value, this.proxy[field], true)) {
308+
this.setFieldValue(field, value)
308309
}
309310
})
310311
},

test/unit/FormulateSchema.test.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,58 @@ describe('FormulateSchema', () => {
160160
}
161161
}
162162
})
163+
})
163164

165+
it('can emit events from children', async () => {
166+
const customBlurHandler = jest.fn()
167+
const directlyCalled = jest.fn()
168+
const changedState = jest.fn()
169+
const wrapper = mount({
170+
template: `
171+
<FormulateForm
172+
:schema="schema"
173+
@blur="customBlurHandler"
174+
@changed-state="changedState"
175+
/>`,
176+
data () {
177+
return {
178+
schema: [
179+
{
180+
name: 'username',
181+
'@input': directlyCalled
182+
},
183+
{
184+
name: 'password',
185+
'@blur': true
186+
},
187+
{
188+
component: 'div',
189+
children: [
190+
{
191+
type: 'select',
192+
name: 'state',
193+
options: ['Kansas', 'Nebraska', 'Iowa'],
194+
value: 'Iowa',
195+
'@change': 'changed-state'
196+
}
197+
]
198+
}
199+
]
200+
}
201+
},
202+
methods: {
203+
customBlurHandler,
204+
changedState
205+
}
206+
})
207+
await flushPromises()
208+
wrapper.find('input[name="username"]').setValue('cooldude45')
209+
wrapper.find('input[name="password"]').trigger('blur')
210+
wrapper.find('option[value="Nebraska"]').setSelected('Nebraska')
211+
await flushPromises()
212+
expect(directlyCalled.mock.calls.length).toBe(1)
213+
expect(customBlurHandler.mock.calls.length).toBe(1)
214+
expect(customBlurHandler.mock.calls[0][0]).toBeInstanceOf(FocusEvent)
215+
expect(changedState.mock.calls.length).toBe(1)
164216
})
165217
})

0 commit comments

Comments
 (0)