-
-
Notifications
You must be signed in to change notification settings - Fork 102
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
feat(resource): static convention #614
feat(resource): static convention #614
Conversation
I really like this idea!
Just my 2c. |
@bigopon could you update above samples with one using JSX for the view? |
@zewa666 Sure, you can also play with it at https://stackblitz.com/edit/aurelia-static-convention?file=app.js @fkleuver I think array gives better config API too, you can have a play in demo link above |
@bigopon Will this only work when the properties are functions (seems that way right now)? |
@bigopon I'd love to support both static methods and properties if possible. |
No it can be either object or string or function resolving to one of those. Static method is what works today without transpiler so i used it |
I can see the good will of supporting jsx as inline template, but the side effect is some unnecessary learning curve. Now for users want to use inline template, they need to understand how jsx works, and also how to map its features to Aurelia. We don't need that learning curve. Instead of: static view () {
return <template>
<input datePicker$='date' />
<button type$="type" onClick='shout()'>Normal button</button>
{'${message}'}
</template>;
} We can just write: static view () {
return `<template>
<input date-picker="date">
<button type="type" click.trigger="shout()">Normal button</button>
\${message}
</template>`;
} There are two minor issues around using es6 multiline string literal.
To make it better, but instead of using babel plugins
This is actually technically simpler than trying to mock
So we can finally write: static view () {
return <template>
<input date-picker="date">
<button type="type" click.trigger="shout()">Normal button</button>
${message}
</template>;
} |
@huochunpeng jsx was purely out of interest to see whether it's doable. It would also provide a nicer migration for existing React code. So I definitely don't envision to push users into JSX direction, more just to know that it kinda works |
That makes sense. I can see from stackblitz jsx is implemented purely by a mock of React.createElement. |
A custom plugin to display html in jsx is awesome, but would be a lot of work I believe. I have no experience of that, maybe @eriklieben, who has been working on vscode extension, can give some comments. Personally I believe complex markup needs to be in their own well formatted file. So final usage would be a plugin to inline the template during build time (single file module / component etc), or devs explicitly import view in the view model file and use it. Though inline view with template literal is very good for demos / getting started. Also another way is to get it directly from <!-- index.html -->
<body>
<div id="app-main-template">
// heavy markup prerender by server
</div>
</body> class App {
static view() {
let template = document.createElement('template');
template.content.appendChild(document.querySelector('#app-main-template'));
return template;
}
// ...
} |
@EisenbergEffect Tests for all new features added and ready for review. |
As long as not using webpack, this can be a small gulp plugin to find matching vinyl files (js and html) and merge them, at gulp stream flush stage. |
Had a brief talk with @bigopon about sub-class support, glad to know it all works. Here is some code example for reference. class AnotherButton extends NormalButton {
static resource() {
let res = super.resource();
res.name = 'another-button';
return res;
}
shout() {
super.shout();
alert('Actually clicked another button :-)');
}
} |
@bigopon Since things have changed a bit in the latest set of iterations, can you update this issue with the full list of PRs, the order they need to go in and any other relevant information? I can start with "ring 0" this week and we can release them all over the next 2-3 weeks, while we work on some documentation. |
@EisenbergEffect i have updated the PR with the road map how things go together |
Thanks! |
does this pr still work with decorators like #606 did? |
@obedm503 New check was added for export function view(config: string | IStaticViewConfig): any {
return function deco(target: Function) {
target.view = config;
}
} @EisenbergEffect should be provide a decorator ? |
It'd be nice to have. I prefer decorators to static properties |
Probably good idea to include that decorator source code in the docs as well. It's nice to define your own decorators in larger apps as a way to apply common logic/conventions and stuff. More people should know about it. |
// it's a custom element, with name is the resource variable | ||
// static resource = 'my-element' | ||
resource = new HtmlBehaviorResource(); | ||
resource.elementName = config; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In main codepath, name (here config
) goes through _hyphenate
.
Here it doesn't, is that on purpose?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🐛 😱
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rethinking about it, probably I will have to do it differently: if name
was specified, use it and lower case it, otherwise hyphenate the class name. It's more consistent with current way how it works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. It's strange that we modify an explictely defined name.
Hypenate made perfect sense when we derived the element from class name.
if (resourceTypeMeta) { | ||
if (resourceTypeMeta instanceof HtmlBehaviorResource) { | ||
if (resourceTypeMeta.attributeName === null && resourceTypeMeta.elementName === null) { | ||
//no customeElement or customAttribute but behavior added by other metadata |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: No space after //
, custome
typo.
//no customeElement or customAttribute but behavior added by other metadata | ||
HtmlBehaviorResource.convention(impl.name, resourceTypeMeta); | ||
} | ||
if (resourceTypeMeta.attributeName === null && resourceTypeMeta.elementName === null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be moved inside previous if
?
Same check twice in a row just looks weird.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's the way we check for custom element and custom attribute.
Try with convention first, if nothing matches > fallback to element
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand what it does but it looks weird. Basically you do:
if (A) {}
if (A) {}
If the first A
is false, the second has to be false as well and is redundant.
Nitpicking anyway, it works fine.
HtmlBehaviorResource.convention(impl.name, resourceTypeMeta); | ||
} | ||
if (resourceTypeMeta.attributeName === null && resourceTypeMeta.elementName === null) { | ||
//no convention and no customeElement or customAttribute but behavior added by other metadata |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: copy-pasted comment with same defects as previous one.
export class StaticViewStrategy { | ||
|
||
template: string | HTMLTemplateElement; | ||
dependencies: Function[] | { (): Function[] | (Function | Promise<Function[] | Record<string, Function>>)[] }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I get this based on previous discussions we had.
But it really needs some explanations and some tidy-up (notice for example Function[]
is redundant in your return type).
If I get this right, you 100% need to explain that:
- it returns an array of promises rather than a promise for an array to easily support
[() => import("x"), () => import("y")] pattern
. - that the
Record<string, Function>
is meantfor lazy loading based on resource name, so theOK, now I think it's for ES modules fromFunction
here is a factory unlike all otherFunction
in that declaration, which are class ctor.() => import("x")
, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might want a factory returning a single resource, i.e. a function?
It might be a typo but it's not in the typedef above.
There are better use cases, but this: () => [ async () => (await import("x")).MyElement ]
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, Record<string, any>
is for import('x')
. That said, I just realized it's doesn't handle the case where an export is not a function. Probably we should ignore any export value that is not a function.
return Promise.resolve(this.factory); | ||
} | ||
let deps = this.dependencies; | ||
deps = typeof deps === 'function' ? deps() : (deps ? Array.isArray(deps) ? deps : [deps] : []); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: no, thanks.
You did get the precedence of everything right, but that's too much trivia for real code.
I suggest at least reordering the checks so that there is no ambiguity in the ternary operators results:
isFunction ? deps() : isArray ? deps : deps || []
That's still a bit cryptic to me, but at least there's no ambiguity in results.
Note that it works because isArray
simply returns false for anything that's not an array, including null
, undefined
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I have fixed this in the followup PR. Reading it again does feel terrible
resource = viewResources.autoRegister(container, dep); | ||
} else if (dep && typeof dep === 'object') { | ||
for (let key in dep) { | ||
resource = viewResources.autoRegister(container, dep[key]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So... I don't quite get it.
What's the point in supporting a record if we do nothing with key
?
Is it so that future versions could lazy-load resources when their name is first encountered in a view?
That's a cool future feature, but then I think the best choice is to not accept a record until then.
Because lazy loading is a breaking change, so we don't want people to use this syntax until we actually implement the feature.
Even so, it's not implemented correctly. As lazy-loading implies a factory, a correct eager-loading implementation would be: dep[key]()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wait, I think I get it now...
This is for the case () => import("y")
, isn't it?
The record is actually an ES module?
Makes sense then.
I like how we try to find all resources in said module, rather than just the first one as in older Aurelia code 👍
No reliance on default
, then.
Worthy of a short explanation or comment, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, It is also consistent with the way we currently load resources if it was string: every thing will be registered. And sure, docs for this work gonna be fun...
@bigopon I recall you needed some resolution on this to move forward. Gitter is broken for me right now so I haven't been able to read your messages or respond to you ;( How can I help move this forward? |
Summary from @jods4 & @StrahilKazlachev 's review Checking for a static method / field I think we may need to replace it with symbol, e.g class MyElement {
static [Symbol.for('au-view')] = '...'
// or
static [Symbol.for('au-view')]() {
// ...
}
} Same thing may apply for |
Ok, I thought that was the main issue. I'd prefer not to use a Symbol though. That makes for a messy api. One thought I had was to make this an opt-in framework feature. Can we have a setting somewhere, that only checks for the static fields if you have enabled it? That would allow us to keep the api we want while providing backward compat. |
That will work. Then how would you suggest for interop between static opt in and decorators ? |
If someone uses a decorator that would create a static prop, but they haven't enabled the feature, we should throw with a clear error message explaining that they need to opt into the new model and how to do that. |
There is probably no reason why using decorators only would not work, should throwing error would be unexpected ? Or you suggesting that we need a strong switch for mental model so folks can be aware of side effect of these static features ? |
I was thinking that you thought there would be clash between the two, so I was recommending a potential solution. I may not have understood your question then... |
Oh I see what you meant. Because decorators resolves to static field, that's why you suggest throwing an error. I was thinking decorator could generate some metadata, while static field ... declare its static bit and we make them interop-able. But we could do as you suggested, always resolve to static method / field and add a flag to opt it in. |
Yep :) |
@EisenbergEffect I know I can't have it both ways: back-compat and easy use of new releases... But still I'm not fan of the opt-in API. This way will become the way to go, especially with Webpack and co., but if it's behind a config it won't be obvious and easy to get started. What if we use an inconvenient enough property, so that we're 90% sure it won't break anyone. I previously suggested: class X {
static ["au-view"] = "<img>";
} It's not that bad, but the presence of dash makes it highly unlikely to have been used by someone. |
One more thing I just thought about: we need this enabled by default, somehow. Here's why: This has tremendous value because it would enable us to drop our Webpack plugin in new projects and support many bundlers. This is the main motivation behind this work. To drop the Webpack plugin, we need our core libs to be updated to use this new static conventions. If we update core libs to use this, it's not an opt-in anymore. |
I support what @jods4 suggested. Using a switch also gives plugin authors dilemmas if they wanna use this feature, it will be eventually on for the rest of the application if a plugin decides to turn it on. Unless it also accepts an additional static prop to turn it on and off per class |
But that's on the way to too much config. I honestly don't see a problem with using symbols or even Reflect.metadata as long as it's managed carefully |
@obedm503 Main problem with symbols is they would require a lot more changes (and in some places plain additions, which can cause performance degradation). They aren't discoverable in the same way as normal properties. However I don't see any issues with poor man's Reflect.metadata, which is to just put a |
Right, forgot about symbols not being enumerable |
Why don't we use |
I agree there's a precedent for I would even say that it hints the fact that the field is "magical", which is a rather good thing. Conventions can sometimes be hard to understand for newcomers. So if there's no better proposal... |
Agreed, |
why not something super ugly and even less likely to be already used like // some aurelia file
export enum AureliaMagicFields {
view = '__aurelia__view__'
}
// user - app.ts
export class App {
static [AureliaMagicFields.view] = []
} I recommend this for a few reasons:
I guess it doesn't even have to be an exported enum, just a constant string will do as well. also, if aurelia ever wanted to move to symbols for these kinds of magic/conventional fields, the breakage, from the users' perspective, wouldn't be as bad if they're already using computed fields |
@obedm503 import { view } from "aurelia-symbols";
export class App {
static [view] = "";
} Then I would argue there is absolutely no reason not to go with a symbol instead of a string. |
@jods4 I think it's best form every package to export their own constants instead of a single package with all of them. say this idea was accepted and user in other places, like the lifecycle methods // import {viewConstants} from 'aurelia-templating';
enum viewConstants {
view = '__au-view__', // or = Symbol('au-view')
resource = '__au-resource__', // or = Symbol('au-resource')
attached = '__au-attached__', // or = Symbol('au-attached')
detached = '__au-detached__', // or = Symbol('au-detached')
}
// import {routerContants} from 'aurelia-router';
enum routerConstants {
activate = '__au-activate__', // or = Symbol('au-activate')
deactivate = '__au-deactivate__', // or = Symbol('au-deactivate')
// previously `configureRouter`
configure = '__au-configure__', // or = Symbol('au-configure')'
}
class Component {
static [viewConstants.resource] = 'x-component';
static [viewConstants.view] = '<template><button>Normal button</button></template>';
handleClick = () => {}
[viewConstants.attached]() {
document.addEventListener('click', this.handleClick);
}
[viewConstants.detached]() {
document.removeEventListener('click', this.handleClick);
}
}
class Parent {
router: any; // Router
// previously `configureRouter`
[routerConstants.configure](config, router) {
this.router = router;
}
}
class Page {
static [viewConstants.view] = '<template><x-component></x-component></template>';
[routerConstants.activate](params) {}
[routerConstants.deactivate](params) {}
} just for the record I'm not suggesting @EisenbergEffect @bigopon what do you think of the idea of using contants (strings or symbols) for aurelia-magic/reserved methods and properties? if the idea is green-lit I could work on a pr to use constants for static properties and functions. I'm still not totally sure about lifecycle methods. |
I use symbols exclusively in a plugin where I wrap and intercept the Most libraries use generated/long names when modifying globals or objects they do not own, such as html elements. Aurelia also does this in those cases. When it's the user putting these properties on their own classes, why worry so much about potential conflicts? Users will have more stuff to learn about and more stuff to import. I feel like it's unnecessary bloat but maybe I'm missing something.. |
As I said, when it comes to lifecycle methods I'm not sure it's necessary. But when it comes to something like static view or resource, there IS a potential for conflict since these are new reserved names. It's entirely possible that someone's using one of these names already and by introducing this feature their code will break. The suggestion of using imported constants removes that possibility for breakage |
This PR is a merge of
together with
resource
field, method on class, as a replacement for@customElement
,@customAttribute
,@valueConverter
,@bindingBehavior
,@viewEngineHooks
callsusage examples:
Declaring a custom element
<normal-button/>
,containerless
, with a bindabletype
attributeno type specified = custom element
declaring a custom element '`
declaring a custom attribute
[date-picker]
Declaring an app custom element
The static
resource
will be looked for before traditional convention, such as export name.pros:
<script src="aurelia.umd.js"></script>
and plain javascript classescons
todo:
...
tobe discussed:
static
resource
signatureNot sure if
bindables
should be an object with each property map to a bindable property, or an array of either string or a bindable property config with name in it. With array, name is required if it's not a stringstatic field / method name
resource
was picked because it aligns with internal term of Aurelia, but doesn't look friendly on famework user side. Probablymetadata
could do a better job. Should it bemetadata
instead ofresource
?@EisenbergEffect @jdanyow @jods4
Update 1:
1.1 global resources declaration via classes feat(config): ability to declare global resources by classes framework#858
--- templating-router resources refactor(configure): register resources synchronously templating-router#73
--- templating-resources perf(all): synchronous resources registration templating-resources#340
1.2 Future pr, where
plugin
accepts a configure function, instead of module id, to be called in sequence whenaurelia.start
is called2.1 Ability to use class as first parameter for
Aurelia.setRoot
feat(Aurelia): ability to define root with constructor framework#8762.2 Ability to use class / normal module for router WIP feat(RouterView): ability to use class as moduleId templating-router#75