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

Adds actions to components #1247

Merged
merged 1 commit into from
Mar 24, 2018
Merged

Conversation

jacwright
Copy link
Contributor

@jacwright jacwright commented Mar 16, 2018

Actions add additional functionality to elements within your component's template that may be difficult to add with other mechanisms. Examples of functionality which actions makes trivial to attach are:

  • tooltips
  • image lazy loaders
  • drag and drop functionality

Actions can be added to an element with the use directive.

<img use:lazyload data-src="giant-photo.jpg>

Data may be passed to the action as an object literal (e.g. use:b="{ setting: true }", a literal value (e.g. use:b="'a string'"), or a value or function from your component's state (e.g. add:b="foo" or add:b="foo()").

Actions are defined in a "actions" property on your component definition.

<script>
  export default {
    actions: {
      b(node, data) {
        // do something
        return {
          update(data) {},
          destroy() {}
        }
      }
    }
  }
</script>

A action is a function which receives a reference to an element and optionally the data if it is added in the HTML. This function can then attach listeners or alter the element as needed. The action can optionally return an object with the methods update(data) and destroy().

When data is added in the HTML and comes from state, the action's update(data) will be called if defined whenever the state is changed.

When the element is removed from the DOM destroy() will be called if provided, allowing for cleanup of event listeners, etc.

See #469 for discussion around this feature and more examples of how it could be used.

@codecov-io
Copy link

codecov-io commented Mar 16, 2018

Codecov Report

Merging #1247 into master will increase coverage by 0.07%.
The diff coverage is 97.14%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1247      +/-   ##
==========================================
+ Coverage   91.88%   91.96%   +0.07%     
==========================================
  Files         126      128       +2     
  Lines        4548     4602      +54     
  Branches     1478     1490      +12     
==========================================
+ Hits         4179     4232      +53     
- Misses        153      154       +1     
  Partials      216      216
Impacted Files Coverage Δ
src/validate/js/propValidators/index.ts 100% <ø> (ø) ⬆️
test/parser/index.js 100% <ø> (ø) ⬆️
src/generators/nodes/index.ts 100% <ø> (ø) ⬆️
src/generators/Generator.ts 94.17% <100%> (+0.15%) ⬆️
src/validate/index.ts 89.47% <100%> (+0.18%) ⬆️
src/validate/js/index.ts 82.85% <100%> (ø) ⬆️
src/validate/html/validateElement.ts 90.47% <100%> (+0.57%) ⬆️
src/parse/read/directives.ts 86.66% <100%> (+0.22%) ⬆️
src/generators/nodes/Action.ts 100% <100%> (ø)
src/validate/js/propValidators/actions.ts 50% <50%> (ø)
... and 4 more

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 bc416a5...04f5d5c. Read the comment docs.

import Text from './Text';
import * as namespaces from '../../utils/namespaces';
import Behavior from './Behavior';
Copy link
Contributor

Choose a reason for hiding this comment

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

two same imports import Behavior from './Behavior'; ^

@Rich-Harris
Copy link
Member

Damn, @jacwright! This looks awesome. The PR is great on an initial read — just one bit of feedback, which is that generally it's better to add a runtime test than a js test, because a) it guarantees the correctness of the generated code over time, and b) there are fewer tests to update whenever we make changes that require the files in js/samples to be updated.

Also, we could make the generated code a tiny bit simpler:

-link_behavior = link(a);
+link_behavior = link(a) || {};

-if (link_behavior && typeof link_behavior.destroy === 'function') {
-  link_behavior.destroy();
-}
+if (link_behavior.destroy) link_behavior.destroy();

Onto the bikeshedding: one problem with 'behavior' is that's the terminology we use to describe all of a component's encapsulated logic — methods, transitions, etc. Also, it's spelt 'behaviour' 😀.

Re-reading #469, I like the 'trait' suggestion for its English-language connotations, but less so for its connotations in programming. I also like as but you're probably right, it doesn't work in all contexts. So just to be awkward, I'm going to make an entirely new suggestion:

<img with:lazyload data-src='potato.jpg'>

<script>
  export default {
    augmentations: {
      lazyload(node, data) {...}
    }
  };
</script>

Any thoughts, anyone?

I also want to make sure we fully think through the lifecycle stuff, because it'd be difficult to make changes in future. Are there situations where you might want to run code before the element enters or exits the DOM? I like the simplicity of update and destroy, but I wonder if perhaps it should be something like mount, update, beforeunmount and unmount instead. (beforemount would be the code that runs when it's first created.)

I can think of two fairly contrived examples off the top of my head — if you had <img with:lazyload src='whatever.jpg'>, then you'd want to remove the src attribute before mounting so that you could control when the loading happened. (Obviously there is an easy workaround there, am just thinking aloud.) Or you might want to preserve information about the node's layout while it's still in the DOM, so that you can apply a transition effect to an incoming component after the outgoing component has been destroyed (a la ramjet — excuse the currently-broken demo).

CC'ing @evs-chris — Chris, do you have any thoughts on the lifecycle stuff? Any lessons from Ractive you could share?

@paulocoghi
Copy link
Contributor

paulocoghi commented Mar 16, 2018

Any thoughts, anyone?

What do you think of one of these: "actions", "enhancements", "additions" or "extensions"? My vote goes to "actions". It is short but it has meaning, as do the others.

@jacwright
Copy link
Contributor Author

jacwright commented Mar 16, 2018

I like traits and with. I say we go with that or follow an existing convention and do behaviors and as.

I also like the simplicity of what is there with just update and destroy. We can add additional lifecycle events later if we need. The one thing that will break backwards compatibility is if we change when the initial method is run. Right now it is before mount, so I think we're ok. The default being before mount makes sense to me (many traits won't care about the DOM). What if we start with these options on the returned object:

  • mount
  • update
  • unmount (before leaving the dom)
  • destroy

@jacwright
Copy link
Contributor Author

jacwright commented Mar 16, 2018

A problem I just thought about is hydration. You can't add a trait to an element before it is in the DOM under all circumstances. If your page is server-rendered you only get the element after it is in the page. Should we account for that and just provide update and destroy and then maybe, in the future, unmount if a use-case turns up needing it? Or do we allow all the lifecycle events and those who are using server-rendered can account for that?

@tomcon
Copy link

tomcon commented Mar 16, 2018

From my pov, am not so keen on:

  1. traits (as per Rich language connotations)
  2. augmentations - too verbose
  3. with (no clear purpose when you read the word + language connotations)

Looking at your opening statement @jacwright :
"Behaviors add additional functionality to elements"
http://www.thesaurus.com/browse/functionality?s=t
= performance, process, purpose, service, use, component
Therefore this feels good imo

<img use:lazyload data-src='potato.jpg'>

<script>
  export default {
    using: {
      lazyload(node, data) {...}
    }
   or 
    uses: {
      lazyload(node, data) {...}
    },
  };
</script>

Also, surprised to hear @Rich-Harris actually wanting to add more events than absolutely necessary at any given time. Normally it is the other way around!

@jacwright jacwright changed the title Adds behaviors to components Adds traits to components Mar 17, 2018
@jacwright
Copy link
Contributor Author

I've made the changes request @Rich-Harris.

  • renamed behaviors to traits
  • renamed add:xyz to with:xyz
  • removed most JS tests and added runtime tests instead
  • added additional lifecycle methods, mount and unmount for cases that need them

Note: elements that hydrate will not benefit from pre-mounting logic. I'm not sure if addEventListener and other DOM methods are available on the server or whether allowing traits to run on the server will cause issues. Originally my thinking was they were only client-side.

@paulocoghi
Copy link
Contributor

Both "services" and "use" (using, uses) are interesting!

@jacwright
Copy link
Contributor Author

@tomcon I like use. But we would still need a noun to reference them by in the docs or libraries. I think decorators and traits are fair game, even if they mean something else at the language level. As it stands now, this PR is using traits and with. I would like to come to a consensus or have @Rich-Harris make a call on it. The more important thing to me is having the functionality this provides.

@Rich-Harris
Copy link
Member

@tomcon's use and @paulocoghi's actions are excellent suggestions. I'd vote for that combo:

<img use:lazyload data-src='potato.jpg'>

<script>
  export default {
    actions: {
      lazyload(node, data) {...}
    }
  };
</script>

How does that sound for everyone?

A problem I just thought about is hydration. You can't add a trait to an element before it is in the DOM under all circumstances.

Gah, you're absolutely right. Having a pre-mount hook is liable to cause all sorts of confusion. Better to not have it at all than to have subtle behaviour differences that people will struggle to debug. And since unmount is something that could be added later as a non-breaking change, I'm going to revert to type (@tomcon gets me) and suggest we file unmount under YAGNI for now. So, back to update and destroy? Sorry for the back and forth on this!

@paulocoghi
Copy link
Contributor

How does that sound for everyone?

Sounds great to me!

@jacwright jacwright changed the title Adds traits to components Adds actions to components Mar 19, 2018
@jacwright
Copy link
Contributor Author

Using actions and use. Only supporting update and destroy. Rebased and squashed to a single commit to keep it clean. I believe this PR is ready except for the following:

Getting errors on validation tests where:
Cannot read property 'line' of undefined should be Missing action 'whatever'
Cannot read property 'line' of undefined should be Actions can only be applied to DOM elements, not components

I cannot reproduce locally, even after a fresh npm install and clean test directory and switching to node 6. It passes every time for me. Any ideas? I assume the error is from warning.loc being undefined, but without reproing I can't troubleshoot.

@Rich-Harris
Copy link
Member

Nice! I would guess the test failures are related to #1250 — validator.warn and validator.error now take a Node (or { start, end } object) as the second argument, rather than a number representing start

if (!validator.actions.has(attribute.name)) {
validator.error(
`Missing action '${attribute.name}'`,
attribute.start
Copy link
Member

Choose a reason for hiding this comment

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

this should now just be

validator.error(`Missing action '${attribute.name}'`, attribute)

due to #1250

if (prop.value.type !== 'ObjectExpression') {
validator.error(
`The 'actions' property must be an object literal`,
prop.start
Copy link
Member

Choose a reason for hiding this comment

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

see previous comment — prop, instead of prop.start

Actions add additional functionality to elements within your component's template that may be difficult to add with other mechanisms. Examples of functionality which actions makes trivial to attach are:
* tooltips
* image lazy loaders
* drag and drop functionality

Actions can be added to an element with the `use` directive.

```html
<img use:lazyload data-src="giant-photo.jpg>
```

Data may be passed to the action as an object literal (e.g. `use:b="{ setting: true }"`, a literal value (e.g. `use:b="'a string'"`), or a value or function from your component's state (e.g. `add:b="foo"` or `add:b="foo()"`).

Actions are defined in a "actions" property on your component definition.

```html
<script>
  export default {
    actions: {
      b(node, data) {
        // do something
        return {
          update(data) {},
          destroy() {}
        }
      }
    }
  }
</script>
```

A action is a function which receives a reference to an element and optionally the data if it is added in the HTML. This function can then attach listeners or alter the element as needed. The action can optionally return an object with the methods `update(data)` and `destroy()`.

When data is added in the HTML and comes from state, the action's `update(data)` will be called if defined whenever the state is changed.

When the element is removed from the DOM `destroy()` will be called if provided, allowing for cleanup of event listeners, etc.

See sveltejs#469 for discussion around this feature and more examples of how it could be used.
@jacwright
Copy link
Contributor Author

All done. Ready to merge.

@PaulMaly
Copy link
Contributor

Does it look like a decorator plugin in Ractive, right?

@jacwright
Copy link
Contributor Author

@PaulMaly exactly.

@constgen
Copy link

constgen commented Mar 23, 2018

Tooltips, image lazy loaders, drag and drop functionality - these cases can be handled by components composition or component wrapper, do they?
Example:

<Tooltip title="This is image"><img src="potato.jpg" /></Tooltip>
<LazyImage src="potato.jpg"></LazyImage>

Lets not extend the framework with yet another syntax

@jacwright
Copy link
Contributor Author

jacwright commented Mar 23, 2018 via email

@Rich-Harris Rich-Harris merged commit e77988b into sveltejs:master Mar 24, 2018
@Rich-Harris
Copy link
Member

Released as 1.58.2. Thank you @jacwright!

@ekhaled
Copy link
Contributor

ekhaled commented Mar 25, 2018

Can someone please post how to use this feature in full before it gets documented on the main website.

Whats the final syntax etc.

@Conduitry
Copy link
Member

@ekhaled The syntax is pretty well described in the initial PR description, but you can also take a look at the action-* tests in https://github.com/sveltejs/svelte/tree/master/test/runtime/samples

@ekhaled
Copy link
Contributor

ekhaled commented Mar 25, 2018

Thanks @Conduitry.
As you can see many variations were discussed such as with and enhancements etc. So wasn't sure what was settled on finally.

@jacwright jacwright deleted the behaviors branch March 26, 2018 15:13
@jacwright
Copy link
Contributor Author

@ekhaled yeah, I updated the description to the latest decision so people didn't have to wade through the comments. I should have made a note that it was updated, sorry about that!

@jacwright
Copy link
Contributor Author

Also, this PR should land soon too #1279 which adds some nuance to actions that ought to be included in the docs.

@constgen
Copy link

So actions are separate modules or I should duplicate them on every component?

@jacwright
Copy link
Contributor Author

They can be separate modules. I hope we (the community) can build up some great actions in NPM that can be used by the community.

jacwright added a commit that referenced this pull request Aug 14, 2018
Looking at the discussion on #1247 it sounds like this was the intended way actions would be set up to work (which is why we didn't add a `mount` lifecycle method). I *believe* this is a fix in the original implementation.

Complaints in chat about this surfaced the issue. Some libraries expect the element to be in the DOM when initializing and these libraries cannot be used without any lifecycle hook. @PaulMaly is requesting this be looked at, and I agree with his assesment.

What's more, this change *should* be backwards compatable. Actions which work before this change should continue working after this change.
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.

10 participants