Skip to content
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

Use custom events for inputs #42

Merged
merged 7 commits into from
Mar 21, 2019
Merged

Use custom events for inputs #42

merged 7 commits into from
Mar 21, 2019

Conversation

drwpow
Copy link
Contributor

@drwpow drwpow commented Mar 20, 2019

2019-03-20 18-21-49 2019-03-20 18_24_41

Reason for change

Use Stencil’s Events API to listen to feature input changes, and keep track of latest state in parent component (for use in pricing API, which will come in a followup PR; this is just about tracking the state for now)

Why is this better than using state?
I really like being able to use uncontrolled inputs for performance, as well as minimize the amount of state & prop drilling.

Testing

  1. When changing events, every input change should log the current state to the console
  2. When changing plans, the new defaults should override the user settings

I didn’t add tests to this PR. While working on it, I had an idea for testing that would require a refactor. I’d rather add tests in a followup PR that (hopefully) cleans up a lot of the custom plan logic. I don’t think I can necessarily reduce the amount of logic there, but I had some ideas on how to at least make it easier to read, and easier to test.

@drwpow drwpow added the wip Work in Progress label Mar 20, 2019
@drwpow drwpow had a problem deploying to manifold-ui-stage-pr-42 March 20, 2019 12:56 Failure
@drwpow drwpow temporarily deployed to manifold-ui-stage-pr-42 March 20, 2019 22:39 Inactive
@manifold-bot
Copy link

manifold-bot commented Mar 20, 2019

Codecov Report

Merging #42 into master will decrease coverage by 65.55%.
The diff coverage is 42.85%.

Impacted file tree graph

@@             Coverage Diff             @@
##           master      #42       +/-   ##
===========================================
- Coverage     100%   34.44%   -65.56%     
===========================================
  Files           1        4        +3     
  Lines           2       90       +88     
  Branches        0       31       +31     
===========================================
+ Hits            2       31       +29     
- Misses          0       59       +59
Impacted Files Coverage Δ
src/spec/mock/catalog.ts 100% <100%> (ø)
src/components/plan-details/plan-details.tsx 27.5% <41.17%> (ø)
src/utils/currency.ts 50% <0%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1833266...044e85d. Read the comment docs.

@drwpow drwpow force-pushed the dangodev/7596-feature-state branch from f22e02b to 8146363 Compare March 21, 2019 00:21
@drwpow drwpow temporarily deployed to manifold-ui-stage-pr-42 March 21, 2019 00:21 Inactive
@drwpow drwpow changed the title In-progress: use custom events for inputs Use custom events for inputs Mar 21, 2019
@drwpow drwpow removed the wip Work in Progress label Mar 21, 2019
@Prop() required?: boolean;
@Prop() onChange: (e: UIEvent) => void;
@Event() onInputChange: EventEmitter;
Copy link
Contributor Author

@drwpow drwpow Mar 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first, I was hoping we could pass an eventName as a prop to the component, and use that here. But the way the custom events work, this more or less has to be a hard-coded value.

So instead, I have a generic onInputChange event being submitted so we can re-use these inputs everywhere.

On the listener side, it’s accepting all these events, but filtering out the ones it doesn’t care about. So basically, if we don’t care about listening to certain inputs we just don’t create a listener for them.

return null;
}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started out trying to make changes in this file, then I found myself passing so many props back-and-forth it just seemed simpler to delete it 🤷‍♂️.

It made plan-details.tsx longer, but I’d like to refactor that in a followup PR to abstract a lot of the display logic into utility functions, and make that component far easier to read.

if (!e.target) return;
const { value } = e.target as HTMLSelectElement;
this.onInputChange.emit({ name: this.name, value });
};
Copy link
Contributor Author

@drwpow drwpow Mar 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the event details, we’re submitting the name of the input so that above in the listener, we can filter out the events with names we don’t care about.

@Event() onInputChange: EventEmitter;
@Watch('defaultValue') watchHandler(newVal: string) {
this.onInputChange.emit({ name: this.name, value: newVal });
}
Copy link
Contributor Author

@drwpow drwpow Mar 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Watch is necessary here only because of the plan change.

The defaultValue is deterministic, and won’t ever update as the user changes input values. It will change, however, if a user switches plans because each plan can have its own defaults. So this is basically a way of emitting a “reset” event to the listener when the plan changes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was totally wondering if @Watch members allowed you to reference this, because the docs aren't really clear on that. 💯


@Component({
tag: 'mf-select',
styleUrl: 'mf-select.css',
scoped: true,
})
export class MfSelect {
@Prop() options: Option[] = [];
@Prop() selectedValue?: Value;
@Prop() defaultValue?: string;
Copy link
Contributor Author

@drwpow drwpow Mar 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Borrowed the defaultValue from React. Technically, this works like value, and can overwrite the user’s changes. But the way it’s passed down, it’ll never change so it should allow the user to modify inputs freely (kinda like how HTML works—value only determines the default. Actually there have been discussions to change this in React)

@drwpow drwpow force-pushed the dangodev/7596-feature-state branch from 8146363 to 67ee295 Compare March 21, 2019 00:34
@drwpow drwpow temporarily deployed to manifold-ui-stage-pr-42 March 21, 2019 00:35 Inactive
@drwpow drwpow self-assigned this Mar 21, 2019
describe(`<plan-details>`, () => {
it('sets its initial features correctly', async () => {
const page = await newE2EPage({ html: `<plan-details />` });
await page.$eval('plan-details', (elm: any) => {});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this doing anything right now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I was going to write some tests then got distracted trying to refactor too much. I want to pull out some of that display logic and test it separately, but there are definitely still tests I can still write here. I’ll add those.

@drwpow drwpow force-pushed the dangodev/7596-feature-state branch from 67ee295 to 2969a78 Compare March 21, 2019 13:37
@drwpow drwpow temporarily deployed to manifold-ui-stage-pr-42 March 21, 2019 13:38 Inactive
@drwpow drwpow temporarily deployed to manifold-ui-stage-pr-42 March 21, 2019 15:31 Inactive
storage: 5,
});
});
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, it gets picked up in the code coverage too :)

Copy link

@davejsdev davejsdev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

@drwpow drwpow temporarily deployed to manifold-ui-stage-pr-42 March 21, 2019 16:03 Inactive
@drwpow drwpow temporarily deployed to manifold-ui-stage-pr-42 March 21, 2019 16:18 Inactive
};
// TODO: replace this with pricing calculation call
console.log(this.features);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sslotsky alternative to @Listen. Seems a bit cleaner because it’s tied directly to the inputs and we no longer do the filtering like you suggested. WYT?

@drwpow drwpow temporarily deployed to manifold-ui-stage-pr-42 March 21, 2019 16:46 Inactive
@drwpow drwpow force-pushed the dangodev/7596-feature-state branch from 62e5d4b to ef5db1a Compare March 21, 2019 16:52
@drwpow drwpow temporarily deployed to manifold-ui-stage-pr-42 March 21, 2019 16:52 Inactive

// TODO: extract these into utils/ to be tested
getBooleanDefaultValue(value: Catalog.FeatureValueDetails): boolean {
return value.label === 'true' || false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the || false actually needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah not anymore. In our spec, confusingly, feature.value may sometimes be missing, and it’s honestly no wonder why we’ve had such a hard time with data. The way this got refactored now, though, I’m handling the case where feature.value isn’t defined earlier so this is no longer needed.

min={min}
name={feature.label}
onUpdateValue={({ detail: { name, value } }: CustomEvent) =>
this.setFeature(name, value)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this.setFeature ever called directly, or are we always receiving the detail object and then destructuring it before calling setFeature? If we never call it directly, could we just redefine the setFeature function to accept the detail object?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could; in my brain it just seemed confusing to have event-specific params passed to a method that is setting internal state. But yeah we’re not testing this or calling it elsewhere (nor do I plan to), so just a naming opinion, I guess. I can rename that function & let it handle custom events.

Copy link
Contributor

@sslotsky sslotsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Life's a garden, baby, dig it!

A couple small, non-blocking suggestions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants