Skip to content

Commit

Permalink
fix(validated-input): rewrite to glimmer and support nested changesets (
Browse files Browse the repository at this point in the history
#581)

* fix(validated-input): rewrite to glimmer and support nested changesets

Previously the computed `_val` did not recompute if `name` was a nested property like `titleObject.de`.
This is solved with using getters. Example twiddle:
https://ember-twiddle.com/9cb7325ea5aa8983b5ea871366014d2d?openFiles=controllers.application%5C.js%2C#docs():

* test!: drop ember 3.16 and legacy-changeset tests
BREAKING CHANGE: This drops support for Ember LTS 3.16 and `ember-changeset` < 3.0.0 and `ember-changeset-validations` < 3.0.0

* refactor(validated-input): refactor dynamic component call to angle-brackets

* chore(*): drop node v10 support

BREAKING CHANGE: drop node v10 support since v10 has reached EOL

* fix(themed-component): convert array to string befor using in key path
  • Loading branch information
velrest authored May 18, 2021
1 parent 46b0d26 commit 2f3e7c5
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 158 deletions.
15 changes: 7 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ jobs:

steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: 10
node-version: 14

- name: Install dependencies
run: yarn install
Expand All @@ -36,9 +36,9 @@ jobs:

steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: 10
node-version: 14

- name: Install dependencies (no lockfile)
run: yarn install --no-lockfile
Expand Down Expand Up @@ -68,15 +68,14 @@ jobs:
matrix:
scenario:
- ember-lts-3.20
- ember-lts-3.16
- ember-lts-3.24
- ember-release
- legacy-changeset

steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: 10
node-version: 14

- name: Install dependencies
run: yarn install
Expand Down
19 changes: 8 additions & 11 deletions addon/-private/themed-component.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { getOwner } from "@ember/application";
import { get, computed } from "@ember/object";
import { computed, get, set } from "@ember/object";

export default (component) => {
return computed({
get() {
const parts = component.split("/");
const [, ...componentNameParts] = parts;
const componentNameParts = parts.slice(1, parts.length).join("/");

if (this.get(`overrideComponents.${componentNameParts}`)) {
return this.get(`overrideComponents.${componentNameParts}`);
if (get(this, `overrideComponents.${componentNameParts}`)) {
return get(this, `overrideComponents.${componentNameParts}`);
}

const config =
Expand All @@ -21,10 +21,7 @@ export default (component) => {
: {};

const theme = config.theme;
const defaultComponent = get(
config,
`defaults.${componentNameParts.join("/")}`
);
const defaultComponent = get(config, `defaults.${componentNameParts}`);

const name = parts.pop();
const basePath = parts.join("/");
Expand All @@ -35,10 +32,10 @@ export default (component) => {
);
},
set(key, value) {
if (!this.get(`overrideComponents`)) {
this.set(`overrideComponents`, {});
if (!get(this, `overrideComponents`)) {
set(this, `overrideComponents`, {});
}
this.set(`overrideComponents.${key}`, value);
set(this, `overrideComponents.${key}`, value);
return value;
},
});
Expand Down
135 changes: 72 additions & 63 deletions addon/components/validated-input.js
Original file line number Diff line number Diff line change
@@ -1,90 +1,99 @@
import Component from "@ember/component";
import { computed, defineProperty } from "@ember/object";
import { v4 } from "uuid";
import { setComponentTemplate } from "@ember/component";
import { action, set, get } from "@ember/object";
import { guidFor } from "@ember/object/internals";
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";

import themedComponent from "../-private/themed-component";
import layout from "../templates/components/validated-input";
import template from "../templates/components/validated-input";

/**
* This component wraps form inputs.
*
* It can be used in a two-way-binding style like
* {{validated-input model=model name='firstName'}} (model will be updated)
* <ValidatedInput @model={{model}} @name='firstName'/> (model will be updated)
*
* or in a one-way-binding style
* {{validated-input model=model name='firstName' on-update=(action "update"}}
* <ValidatedInput @model={{model}} @name='firstName' @on-update={{this.update}}
* (update action is called, model is not updated)
*
* @class validated-input
* @export default
*/
export default Component.extend({
layout,
tagName: "",
dirty: false,
required: false,
type: "text",
validateBeforeSubmit: true,

init(...args) {
this._super(...args);

defineProperty(
this,
"_val",
computed("value", `model.${this.name}`, "name", function () {
return this.value || this.get(`model.${this.name}`);
})
);
},
export class ValidatedInput extends Component {
inputId = guidFor(this);

@tracked dirty;
@tracked required;
@tracked type;
@tracked validateBeforeSubmit;

@themedComponent("validated-input/render") renderComponent;
@themedComponent("validated-input/label") labelComponent;
@themedComponent("validated-input/hint") hintComponent;
@themedComponent("validated-input/error") errorComponent;

inputId: computed(function () {
return v4();
}),
constructor(...args) {
super(...args);

errors: computed("_val", "name", function () {
const errors = this.get(`model.error.${this.name}.validation`) || [];
this.dirty = this.args.dirty ?? false;
this.required = this.args.required ?? false;
this.type = this.args.type ?? "text";
this.validateBeforeSubmit = this.args.validateBeforeSubmit ?? true;

this.renderComponent = this.args.renderComponent ?? this.renderComponent;
this.labelComponent = this.args.labelComponent ?? this.labelComponent;
this.hintComponent = this.args.hintComponent ?? this.hintComponent;
this.errorComponent = this.args.errorComponent ?? this.errorComponent;
}

get _val() {
return (
this.args.value ??
(this.args.model &&
this.args.name &&
get(this.args.model, this.args.name))
);
}

get errors() {
const errors =
(this.args.model &&
get(this.args.model, `error.${this.args.name}.validation`)) ??
[];

if (!Array.isArray(errors)) {
return [errors];
}

return errors;
}),
}

isValid: computed("showValidity", "errors.[]", function () {
get isValid() {
return this.showValidity && !this.errors.length;
}),
}

isInvalid: computed("showValidity", "errors.[]", function () {
get isInvalid() {
return this.showValidity && !!this.errors.length;
}),

renderComponent: themedComponent("validated-input/render"),
labelComponent: themedComponent("validated-input/label"),
hintComponent: themedComponent("validated-input/hint"),
errorComponent: themedComponent("validated-input/error"),

showValidity: computed(
"validateBeforeSubmit",
"dirty",
"submitted",
function () {
return this.submitted || (this.validateBeforeSubmit && this.dirty);
}

get showValidity() {
return this.args.submitted || (this.validateBeforeSubmit && this.dirty);
}

@action
setDirty() {
this.dirty = true;
}

@action
update(value) {
if (this["on-update"]) {
this["on-update"](value, this.args.model);
} else {
set(this.args.model, this.args.name, value);
}
),

actions: {
setDirty() {
this.set("dirty", true);
},

update(value) {
if (this["on-update"]) {
this["on-update"](value, this.model);
} else {
this.set(`model.${this.name}`, value);
}
},
},
});
}
}

export default setComponentTemplate(template, ValidatedInput);
117 changes: 69 additions & 48 deletions addon/templates/components/validated-input.hbs
Original file line number Diff line number Diff line change
@@ -1,55 +1,76 @@
{{#if hasBlock}}
{{component labelComponent label=label required=required isValid=isValid isInvalid=isInvalid inputId=inputId}}
{{#if (has-block)}}
{{#let (component this.labelComponent) as |LabelComponent|}}
<LabelComponent
@label={{@label}}
@required={{this.required}}
@isValid={{this.isValid}}
@isInvalid={{this.isInvalid}}
@inputId={{this.inputId}}
/>
{{/let}}

{{yield (hash
value=_val
update=(action "update")
setDirty=(action "setDirty")
model=model
name=name
inputId=inputId
isValid=isValid
isInvalid=isInvalid
)}}
{{yield
(hash
value=this._val
update=this.update
setDirty=this.setDirty
model=@model
name=@name
inputId=this.inputId
isValid=this.isValid
isInvalid=this.isInvalid
)
}}

{{#if hint}}
{{component hintComponent hint=hint}}
{{#if @hint}}
{{component this.hintComponent hint=@hint}}
{{/if}}

{{#if (and showValidity errors)}}
{{component errorComponent errors=errors}}
{{#if (and this.showValidity this.errors)}}
{{component this.errorComponent errors=this.errors}}
{{/if}}
{{else}}
{{component renderComponent
type=type
value=_val
inputId=inputId
options=options
name=name
inputName=inputName
disabled=disabled
autofocus=autofocus
autocomplete=autocomplete
rows=rows
cols=cols
model=model
isValid=isValid
isInvalid=isInvalid
placeholder=placeholder
class=class

promptIsSelectable=promptIsSelectable
optionLabelPath=optionLabelPath
optionValuePath=optionValuePath
optionTargetPath=optionTargetPath
includeBlank=includeBlank
multiple=multiple

update=(action "update")
setDirty=(action "setDirty")

labelComponent=(component labelComponent label=label required=required isValid=isValid isInvalid=isInvalid inputId=inputId)
hintComponent=(if hint (component hintComponent hint=hint))
errorComponent=(if (and showValidity errors) (component errorComponent errors=errors))
}}
{{#let (component this.renderComponent) as |RenderComponent|}}
<RenderComponent
@type={{this.type}}
@value={{this._val}}
@inputId={{this.inputId}}
@options={{@options}}
@name={{@name}}
@inputName={{@inputName}}
@disabled={{@disabled}}
@autofocus={{@autofocus}}
@autocomplete={{@autocomplete}}
@rows={{@rows}}
@cols={{@cols}}
@model={{@model}}
@isValid={{this.isValid}}
@isInvalid={{this.isInvalid}}
@placeholder={{@placeholder}}
@class={{@class}}
@promptIsSelectable={{@promptIsSelectable}}
@optionLabelPath={{@optionLabelPath}}
@optionValuePath={{@optionValuePath}}
@optionTargetPath={{@optionTargetPath}}
@includeBlank={{@includeBlank}}
@multiple={{@multiple}}
@update={{this.update}}
@setDirty={{this.setDirty}}
@labelComponent={{
component
this.labelComponent
label=@label
required=@required
isValid=this.isValid
isInvalid=this.isInvalid
inputId=this.inputId
}}
@hintComponent={{if @hint (component this.hintComponent hint=@hint)}}
@errorComponent={{
if
(and this.showValidity this.errors)
(component this.errorComponent errors=this.errors)
}}
/>
{{/let}}
{{/if}}
Loading

0 comments on commit 2f3e7c5

Please sign in to comment.