mirror of
https://github.com/discourse/discourse.git
synced 2024-12-19 05:35:59 +08:00
DEV: adds support for nested collections and objects (#30265)
Collections were an existing concept in FormKit but didn't allow nesting. You can now do infinite nesting: ```gjs <Form @data={{hash foo=(array (hash bar=(array (hash baz=1))) (hash bar=(array (hash baz=2))) ) }} as |form| > <form.Collection @name="foo" as |parent parentIndex|> <parent.Collection @name="bar" as |child childIndex|> <child.Field @name="baz" @title="Baz" as |field|> <field.Input /> </child.Field> </parent.Collection> </form.Collection> </Form> ``` On top of this a new component has been added: `Object`. It allows you to represent objects in your form data. Collections are basically handling arrays, and Objects are objects. This is useful if you form data has this shape for example: ```javascript { foo: { bar: 1, baz: 2 } } ``` This can now be mapped in your form using this syntax: ```gjs <Form @data={{hash foo=(hash bar=1 baz=2)}} as |form|> <form.Object @name="foo" as |object name|> <object.Field @name={{name}} @title={{name}} as |field|> <field.Input /> </object.Field> </form.Object> </Form> ``` Objects accept nested collections and nested objects. Just like Collections. A small addition has also been made to `Collection`, they now support a custom `@tagName`, it's useful if each item of your collection is the row of a table for example.
This commit is contained in:
parent
b191d63c1b
commit
f6a4de4805
|
@ -1,40 +1,78 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { hash } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { concat, hash } from "@ember/helper";
|
||||
import { action, get } from "@ember/object";
|
||||
import FKField from "discourse/form-kit/components/fk/field";
|
||||
import FKObject from "discourse/form-kit/components/fk/object";
|
||||
import element from "discourse/helpers/element";
|
||||
|
||||
export default class FKCollection extends Component {
|
||||
@action
|
||||
remove(index) {
|
||||
this.args.remove(this.args.name, index);
|
||||
this.args.remove(this.name, index);
|
||||
}
|
||||
|
||||
get collectionValue() {
|
||||
return this.args.data.get(this.args.name);
|
||||
get collectionData() {
|
||||
return this.args.data.get(this.name);
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.args.parentName
|
||||
? `${this.args.parentName}.${this.args.name}`
|
||||
: this.args.name;
|
||||
}
|
||||
|
||||
get tagName() {
|
||||
return this.args.tagName || "div";
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="form-kit__collection">
|
||||
{{#each this.collectionValue key="index" as |data index|}}
|
||||
{{yield
|
||||
(hash
|
||||
Field=(component
|
||||
FKField
|
||||
errors=@errors
|
||||
collectionName=@name
|
||||
collectionIndex=index
|
||||
addError=@addError
|
||||
data=@data
|
||||
set=@set
|
||||
registerField=@registerField
|
||||
unregisterField=@unregisterField
|
||||
triggerRevalidationFor=@triggerRevalidationFor
|
||||
{{#let (element this.tagName) as |Wrapper|}}
|
||||
{{#each this.collectionData key="index" as |data index|}}
|
||||
<Wrapper class="form-kit__collection">
|
||||
{{yield
|
||||
(hash
|
||||
Field=(component
|
||||
FKField
|
||||
errors=@errors
|
||||
collectionIndex=index
|
||||
addError=@addError
|
||||
data=@data
|
||||
set=@set
|
||||
registerField=@registerField
|
||||
unregisterField=@unregisterField
|
||||
triggerRevalidationFor=@triggerRevalidationFor
|
||||
parentName=(concat this.name "." index)
|
||||
)
|
||||
Object=(component
|
||||
FKObject
|
||||
errors=@errors
|
||||
addError=@addError
|
||||
data=@data
|
||||
set=@set
|
||||
registerField=@registerField
|
||||
unregisterField=@unregisterField
|
||||
triggerRevalidationFor=@triggerRevalidationFor
|
||||
parentName=(concat this.name "." index)
|
||||
)
|
||||
Collection=(component
|
||||
FKCollection
|
||||
errors=@errors
|
||||
addError=@addError
|
||||
data=@data
|
||||
set=@set
|
||||
registerField=@registerField
|
||||
unregisterField=@unregisterField
|
||||
triggerRevalidationFor=@triggerRevalidationFor
|
||||
parentName=(concat this.name "." index)
|
||||
remove=@remove
|
||||
)
|
||||
remove=this.remove
|
||||
)
|
||||
remove=this.remove
|
||||
)
|
||||
index
|
||||
}}
|
||||
index
|
||||
(get this.collectionData index)
|
||||
}}
|
||||
</Wrapper>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/let}}
|
||||
</template>
|
||||
}
|
||||
|
|
|
@ -146,13 +146,11 @@ export default class FKFieldData extends Component {
|
|||
throw new Error("@name can't include `.` or `-`.");
|
||||
}
|
||||
|
||||
return (
|
||||
(this.args.collectionName ? `${this.args.collectionName}.` : "") +
|
||||
(this.args.collectionIndex !== undefined
|
||||
? `${this.args.collectionIndex}.`
|
||||
: "") +
|
||||
this.args.name
|
||||
);
|
||||
if (this.args.parentName) {
|
||||
return `${this.args.parentName}.${this.args.name}`;
|
||||
}
|
||||
|
||||
return this.args.name;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -57,7 +57,7 @@ export default class FKField extends Component {
|
|||
@registerField={{@registerField}}
|
||||
@format={{@format}}
|
||||
@disabled={{@disabled}}
|
||||
@collectionName={{@collectionName}}
|
||||
@parentName={{@parentName}}
|
||||
as |field|
|
||||
>
|
||||
<this.wrapper @size={{@size}}>
|
||||
|
|
|
@ -14,6 +14,7 @@ import FKErrorsSummary from "discourse/form-kit/components/fk/errors-summary";
|
|||
import FKField from "discourse/form-kit/components/fk/field";
|
||||
import FKFieldset from "discourse/form-kit/components/fk/fieldset";
|
||||
import FKInputGroup from "discourse/form-kit/components/fk/input-group";
|
||||
import FKObject from "discourse/form-kit/components/fk/object";
|
||||
import Row from "discourse/form-kit/components/fk/row";
|
||||
import FKSection from "discourse/form-kit/components/fk/section";
|
||||
import FKSubmit from "discourse/form-kit/components/fk/submit";
|
||||
|
@ -289,6 +290,17 @@ class FKForm extends Component {
|
|||
unregisterField=this.unregisterField
|
||||
triggerRevalidationFor=this.triggerRevalidationFor
|
||||
)
|
||||
Object=(component
|
||||
FKObject
|
||||
errors=this.formData.errors
|
||||
addError=this.addError
|
||||
data=this.formData
|
||||
set=this.set
|
||||
registerField=this.registerField
|
||||
unregisterField=this.unregisterField
|
||||
triggerRevalidationFor=this.triggerRevalidationFor
|
||||
remove=this.remove
|
||||
)
|
||||
InputGroup=(component
|
||||
FKInputGroup
|
||||
errors=this.formData.errors
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { hash } from "@ember/helper";
|
||||
import FKCollection from "discourse/form-kit/components/fk/collection";
|
||||
import FKField from "discourse/form-kit/components/fk/field";
|
||||
|
||||
export default class FKObject extends Component {
|
||||
get objectData() {
|
||||
return this.args.data.get(this.name);
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.args.parentName
|
||||
? `${this.args.parentName}.${this.args.name}`
|
||||
: this.args.name;
|
||||
}
|
||||
|
||||
get keys() {
|
||||
return Object.keys(this.objectData);
|
||||
}
|
||||
|
||||
entryData(name) {
|
||||
return this.objectData[name];
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="form-kit__object">
|
||||
{{#each this.keys key="index" as |name|}}
|
||||
{{yield
|
||||
(hash
|
||||
Field=(component
|
||||
FKField
|
||||
errors=@errors
|
||||
addError=@addError
|
||||
data=@data
|
||||
set=@set
|
||||
registerField=@registerField
|
||||
unregisterField=@unregisterField
|
||||
triggerRevalidationFor=@triggerRevalidationFor
|
||||
parentName=this.name
|
||||
)
|
||||
Object=(component
|
||||
FKObject
|
||||
errors=@errors
|
||||
addError=@addError
|
||||
data=@data
|
||||
set=@set
|
||||
registerField=@registerField
|
||||
unregisterField=@unregisterField
|
||||
triggerRevalidationFor=@triggerRevalidationFor
|
||||
parentName=this.name
|
||||
)
|
||||
Collection=(component
|
||||
FKCollection
|
||||
errors=@errors
|
||||
addError=@addError
|
||||
data=@data
|
||||
set=@set
|
||||
registerField=@registerField
|
||||
unregisterField=@unregisterField
|
||||
triggerRevalidationFor=@triggerRevalidationFor
|
||||
parentName=this.name
|
||||
remove=@remove
|
||||
)
|
||||
)
|
||||
name
|
||||
(this.entryData name)
|
||||
}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -3,11 +3,22 @@ import { click, render } from "@ember/test-helpers";
|
|||
import { module, test } from "qunit";
|
||||
import Form from "discourse/components/form";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import formKit from "discourse/tests/helpers/form-kit-helper";
|
||||
|
||||
module("Integration | Component | FormKit | Collection", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("default", async function (assert) {
|
||||
test("@tagName", async function (assert) {
|
||||
await render(<template>
|
||||
<Form @data={{hash foo=(array (hash bar=1) (hash bar=2))}} as |form|>
|
||||
<form.Collection @name="foo" @tagName="tr" />
|
||||
</Form>
|
||||
</template>);
|
||||
|
||||
assert.dom("tr.form-kit__collection").exists();
|
||||
});
|
||||
|
||||
test("field", async function (assert) {
|
||||
await render(<template>
|
||||
<Form @data={{hash foo=(array (hash bar=1) (hash bar=2))}} as |form|>
|
||||
<form.Collection @name="foo" as |collection|>
|
||||
|
@ -45,4 +56,85 @@ module("Integration | Component | FormKit | Collection", function (hooks) {
|
|||
assert.form().field("foo.0.bar").hasValue("1");
|
||||
assert.form().field("foo.1.bar").doesNotExist();
|
||||
});
|
||||
|
||||
test("nested object", async function (assert) {
|
||||
await render(<template>
|
||||
<Form
|
||||
@data={{hash
|
||||
foo=(array (hash bar=(hash baz=1)) (hash bar=(hash baz=2)))
|
||||
}}
|
||||
as |form|
|
||||
>
|
||||
<form.Collection @name="foo" as |collection index|>
|
||||
<collection.Object @name="bar" as |object|>
|
||||
<object.Field @name="baz" @title="Baz" as |field|>
|
||||
<field.Input />
|
||||
<form.Button
|
||||
class={{concat "remove-" index}}
|
||||
@action={{fn collection.remove index}}
|
||||
>Remove</form.Button>
|
||||
</object.Field>
|
||||
</collection.Object>
|
||||
</form.Collection>
|
||||
</Form>
|
||||
</template>);
|
||||
|
||||
assert.form().field("foo.0.bar.baz").hasValue("1");
|
||||
assert.form().field("foo.1.bar.baz").hasValue("2");
|
||||
|
||||
await click(".remove-1");
|
||||
|
||||
assert.form().field("foo.0.bar.baz").hasValue("1");
|
||||
assert.form().field("foo.1.bar.baz").doesNotExist();
|
||||
|
||||
await formKit().field("foo.0.bar.baz").fillIn("2");
|
||||
|
||||
assert.form().field("foo.0.bar.baz").hasValue("2");
|
||||
});
|
||||
|
||||
test("nested collection", async function (assert) {
|
||||
await render(<template>
|
||||
<Form
|
||||
@data={{hash
|
||||
one=(array
|
||||
(hash two=(array (hash three=(array (hash foo=1) (hash foo=2)))))
|
||||
)
|
||||
}}
|
||||
as |form|
|
||||
>
|
||||
<form.Collection @name="one" as |first firstIndex|>
|
||||
<first.Collection @name="two" as |second secondIndex|>
|
||||
<second.Collection @name="three" as |third thirdIndex|>
|
||||
<third.Field @name="foo" @title="Foo" as |field|>
|
||||
<field.Input />
|
||||
<form.Button
|
||||
class={{concat
|
||||
"remove-"
|
||||
firstIndex
|
||||
"-"
|
||||
secondIndex
|
||||
"-"
|
||||
thirdIndex
|
||||
}}
|
||||
@action={{fn third.remove thirdIndex}}
|
||||
>Remove</form.Button>
|
||||
</third.Field>
|
||||
</second.Collection>
|
||||
</first.Collection>
|
||||
</form.Collection>
|
||||
</Form>
|
||||
</template>);
|
||||
|
||||
assert.form().field("one.0.two.0.three.0.foo").hasValue("1");
|
||||
assert.form().field("one.0.two.0.three.1.foo").hasValue("2");
|
||||
|
||||
await click(".remove-0-0-1");
|
||||
|
||||
assert.form().field("one.0.two.0.three.0.foo").hasValue("1");
|
||||
assert.form().field("one.0.two.0.three.1.foo").doesNotExist();
|
||||
|
||||
await formKit().field("one.0.two.0.three.0.foo").fillIn("2");
|
||||
|
||||
assert.form().field("one.0.two.0.three.0.foo").hasValue("2");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
import { array, concat, fn, hash } from "@ember/helper";
|
||||
import { click, render } from "@ember/test-helpers";
|
||||
import { module, test } from "qunit";
|
||||
import Form from "discourse/components/form";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import formKit from "discourse/tests/helpers/form-kit-helper";
|
||||
|
||||
module("Integration | Component | FormKit | Object", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("field", async function (assert) {
|
||||
await render(<template>
|
||||
<Form @data={{hash foo=(hash bar=1 baz=2)}} as |form|>
|
||||
<form.Object @name="foo" as |object name|>
|
||||
<object.Field @name={{name}} @title={{name}} as |field|>
|
||||
<field.Input />
|
||||
</object.Field>
|
||||
</form.Object>
|
||||
</Form>
|
||||
</template>);
|
||||
|
||||
assert.form().field("foo.bar").hasValue("1");
|
||||
assert.form().field("foo.baz").hasValue("2");
|
||||
});
|
||||
|
||||
test("nested object", async function (assert) {
|
||||
await render(<template>
|
||||
<Form
|
||||
@data={{hash one=(hash two=(hash three=(hash foo=1 bar=2)))}}
|
||||
as |form|
|
||||
>
|
||||
<form.Object @name="one" as |one|>
|
||||
<one.Object @name="two" as |two|>
|
||||
<two.Object @name="three" as |three name|>
|
||||
<three.Field @name={{name}} @title={{name}} as |field|>
|
||||
<field.Input />
|
||||
</three.Field>
|
||||
</two.Object>
|
||||
</one.Object>
|
||||
</form.Object>
|
||||
</Form>
|
||||
</template>);
|
||||
|
||||
assert.form().field("one.two.three.foo").hasValue("1");
|
||||
assert.form().field("one.two.three.bar").hasValue("2");
|
||||
|
||||
await formKit().field("one.two.three.foo").fillIn("2");
|
||||
|
||||
assert.form().field("one.two.three.foo").hasValue("2");
|
||||
});
|
||||
|
||||
test("nested collection", async function (assert) {
|
||||
await render(<template>
|
||||
<Form
|
||||
@data={{hash one=(hash two=(array (hash foo=1) (hash foo=2)))}}
|
||||
as |form|
|
||||
>
|
||||
<form.Object @name="one" as |one|>
|
||||
<one.Collection @name="two" as |two twoIndex|>
|
||||
<two.Field @name="foo" @title="foo" as |field|>
|
||||
<field.Input />
|
||||
</two.Field>
|
||||
<form.Button
|
||||
class={{concat "remove-" twoIndex}}
|
||||
@action={{fn two.remove twoIndex}}
|
||||
>Remove</form.Button>
|
||||
</one.Collection>
|
||||
</form.Object>
|
||||
</Form>
|
||||
</template>);
|
||||
|
||||
assert.form().field("one.two.0.foo").hasValue("1");
|
||||
assert.form().field("one.two.1.foo").hasValue("2");
|
||||
|
||||
await click(".remove-1");
|
||||
|
||||
assert.form().field("one.two.0.foo").hasValue("1");
|
||||
assert.form().field("one.two.1.foo").doesNotExist();
|
||||
|
||||
await formKit().field("one.two.0.foo").fillIn("2");
|
||||
|
||||
assert.form().field("one.two.0.foo").hasValue("2");
|
||||
});
|
||||
});
|
|
@ -1,5 +1,7 @@
|
|||
.form-kit__collection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--form-kit-gutter-y);
|
||||
&div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--form-kit-gutter-y);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
@import "_alert";
|
||||
@import "_char-counter";
|
||||
@import "_col";
|
||||
@import "_object";
|
||||
@import "_collection";
|
||||
@import "_conditional-display";
|
||||
@import "_container";
|
||||
|
|
3
app/assets/stylesheets/common/form-kit/_object.scss
Normal file
3
app/assets/stylesheets/common/form-kit/_object.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
.form-kit__object {
|
||||
display: contents;
|
||||
}
|
Loading…
Reference in New Issue
Block a user