Closed kball closed 7 years ago
Not sure what the best way to track progress on this... does it make sense to break it down by component?
Maybe switch to TypeScript? Than create a bundle with systemJS? Most of the code is already in ES2015 so the switch should be easy and I could improve the code a lot and help while removing jQuery. With TypeScript working with HTMLElements and all the subclasses is a lot easier. This may also be a great opportunity to improve integration of foundation with JS/SPA frameworks.
@DaSchTour interesting suggestion, I'd really like to head the ZURBian opinion.
I'm a little hesitant to move towards Typescript given no one at ZURB is currently using it, however I don't think I know enough at this point to say yes or no. Would be interested in some other opinions... @zurb/yetinauts are any of you on TypeScript?
@DaSchTour can you elaborate a little more on both the step from ES2015 to TypeScript and some more detail on the benefits you see from TypeScript?
@kball TypeScript is simply just ES2015+ with addition of types. First to know is, that every valid JavaScript is also valid TypeScript, so at first there is not much new to learn. The big benefit is, that you get type information and validation before runtime.
So for example if you do something like this document.getElementsByTagName("img");
you know that there is a NodeList of HTMLImageElement and what properties this elements have. If you select elements by id, they are simply HTMLElement and you can then can make a check width instanceof HTMLImageElement
to check if it's the type of element you expected. You can do VanillaJS with pre-runtime verification, that the code will really work.
Another big benefit is, that TypeScript creates some kind of dependency tree inside the project. So can do solid refactoring. The TypeScript transpiler sees what dependencies are use, which are missing or what may not be used anymore.
There are certainly much more advantages. I guess the most important point about TypeScript is, that there is not that much new to learn. Besides types TypeScript is just the same as ES2015, but types enables you to see what you got in your hands before runtime.
Here are some more interesting points: https://scotch.io/tutorials/why-you-shouldnt-be-scared-of-typescript
I am not on TypeScript, and am most likely the old curmudgeon of the group when it comes to things like this. I will say that while I will play that role with my statement, consider it neither an endorsement of something else or deterrent of TypeScript. Feel free to call me whatever silly names you wish ;)
That being said, my concerns with anything outside, around, or on top of vanillaJS is:
I most likely am off base about some of my fears so jump in to sway me in any direction, and I am glad that a JS based language is being talked about here, and not some odd offshoot (too me anyway).
My biggest concern on TypeScript vs not is reducing the accessibility to both existing and new contributors... a quick poll of the Foundation twitter audience shows relatively few are actually using TypeScript:
That said, Foundation has never shied away from adopting new technologies before they were popular, once we decide it's the right way to go.
@DaSchTour I'm interested in learning more about your thoughts around this statement:
This may also be a great opportunity to improve integration of foundation with JS/SPA frameworks.
Beyond the fact that Angular2 is TypeScript native, did you have more in mind for this? Improving the integration/pluggability into SPA frameworks is a big agenda item of mine.
@kball well my impression is, that TypeScript helps with modularization and usage of VanillaJS. Which I think are the most important points to create better pluggability. It's a lot easier to create more complex applications and walk through dependent classes. My idea was, that a certain interface definition would help with building the several components and keep them maintainable.
The other point is, that at the moment I find it very hard to use Foundation in an Angular 2 or a high-level TypeScript environment. But that's maybe just because of the very jQueryish-API. But my hope is, that by using TypeScript the API get's more accessible.
My idea would be that there could be something like a core Foundation-JS/TS, that handles the basic state changes and event emitting of components. Maybe as abstract implementations that than can be used for Adapters to jQuery, Angular, React or what ever. When having classes with let's say 10 Adapters extending them, one for every application framework, every change to the base classes will need very fine permanent unit testing to avoid breaking something. In some way TypeScript is a bit like the basic unit testing. Is the property available, is the interface implemented correctly and so on.
Maybe that's just my special point of view, as by using TypeScript we have managed to improve the stability and maintainability of our project a lot and we didn't even need a special training for any of our team members.
@DaSchTour I'm doing a bit more research and it definitely seems like given the scope of Foundation it might be helpful to use TypeScript, if not immediately then a little ways down the road. I have a couple more questions based on your experience...
Here are some thoughts on these various issues. No hard solutions, because the current codebase doesn't afford us many easy answers. Some of these things might have to be punted to Foundation 7.
To jump in on the TypeScript thing, many of its type-checking features are optional. For example, you can configure the compiler to cast any type-less value to any
, which means "any type is valid". Newer versions of TS also allow you to mix .ts
and .js
files, allowing a codebase to be converted incrementally.
As far as compiling distribution files, I think Webpack is a viable option. Each plugin (and the core file) could be compiled individually down to a UMD bundle. The customizer could also be reworked to use Webpack to compile an entry point file generated based on which modules the user selects. It would be a lot like the Sass customizer file, which is a dynamic list of @import
statements. This would be a dynamic list of JavaScript import
statements.
However, Kevin hit the nail on the head that the organization of the code is a substantial roadblock. The dependency graph for each plugin is pretty specific, and utility functions are stored in a number of places. Some are stored in the same file as the plugin that uses it, some are stored in individual modules, and some are stored in foundation.core.js
. The placement within the Foundation
object is also inconsistent—some functions are stored directly on Foundation
, and some are on Foundation.util
as I recall. Creating UMD bundles with our current system would take a lot of specific configuration.
Ideally, the user wouldn't need to understand the framework's dependency graph to use the framework. In a typical library, you just import
the thing you need, and you don't see all the other files imported behind the scenes. However, people drop individual Foundation files into their browser, or use a tool like gulp-concat to assemble their JavaScript, which makes understanding the dependency graph somewhat necessary.
There's a brute-force approach you could take where each plugin is compiled down to a distribution file with its utility dependencies. This means the plugin is guaranteed to work as long as you drop in foundation.core.js
along with it. However, that creates a ton of duplicate code, because many plugins share dependencies. So it would negatively affect users who:
It's probably enough people that we can't go for the simplest approach.
Last scattered thought for now. I think plugins would possibly need to be fully encapsulated in order to do the module system right. In the current codebase, when a plugin's code is executed, the plugin class is added to a global list of plugins stored in the Foundation
object. So the plugin code has a side effect that happens when you run it.
To fix this however, we'd need to make a breaking change to the API, and it would make the initialization step more verbose. It would look something like this:
import Foundation, { Reveal, Orbit } from 'foundation';
Foundation.use(Reveal, Orbit);
In the above example, the act of importing a plugin doesn't do anything by itself. You have to slot it in manually with a function call. Or maybe the side effects aren't an issue, I dunno. Depending on how we organize the code, however, it could create a circular dependency. For example, the main Foundation
export also exports Reveal
, but Reveal
depends on Foundation
so it can call Foundation.registerPlugin
.
@kball short comment on you points: As @gakimball already said, it's easy to incrementally upgrade to TypeScript. I could create a branch with the first most simple upgrade to TypeScript. Well that depends on which features you use and how the compiler is configured. If this is a concern, it's also possible to use TypeScript together with Babel. Well most people that started with TypeScript could start instantly and the learning curve was somehow like: "Oh, it's just like with JavaScript". The type notation than is mostly like a hint. And even with complex interfaces and generics most people understand how they work, although they don't actively use them. With people coming from Java for example it's somehow like a lightweight Java. And I was surprised what they did with TypeScript without any further training just by using what they know from Java, nearly exactly like they did with Java.
@gakimball I was thinking the same for Foundation 7. TypeScript already supports the latest features of ES, it also provides many other features, particularly in terms of modularity and encapsulation.
@gakimball @abdullahsalem @DaSchTour Do you think switching to TypeScript would help in terms of getting the dependency graph under control? Or are they orthogonal issues?
I'd kind of like a change that large to be in more of a Major release (ie 7, which is tentatively targeted for November 2017), but I think getting our dependency system under control is pretty urgent.
@kball TypeScript helps a lot with dependencies. The compiler tells you immediately if something is missing or an interface changed. It already saved me many times while refactoring.
@kball I think if the current codebase adopted TypeScript, and then we went about our refactor, having a statically-typed codebase would be helpful.
However, I think the initial task of reworking out current module organization will still be just as challenging. There are higher-level, more abstract decisions that need to be made about what goes where, which are relevant regardless of if the codebase is using TypeScript.
Quite apart from the TypeScript decision, I think @gakimball's points around actual approach and delivery are pretty important.
One question I have is... do we need to have a "good" solution for the 'just drop in the files with script tags' for anything other than the "full package"? IE, what if our answer was... use the full version, use a build system, use the customizer, or live with some bloat in your packages? (Also known as for f's sake folks, we're about to be in 2017, you can use a build system... :P) Do we know how many people are BOTH dropping in scriptfiles
Alternatively, can we have some alternate entry points/compilations for a set of distfiles? Maybe put them in a wrapper that does some sort of lazy evaluation of dependencies?
I'd like to make our actual modules fully encapsulated and side-effect-free, which then necessarily entails some change in the API. That said, I'd like to hide as much of that from the naive user as possible... if you drop in all of foundation, or build a custom Foundation package with the customizer, you should still be able to just initialize Foundation on the page.
I'm wondering if we can do this by putting a thin shim layer or method on Foundation core that you pass a list of modules and it does the work of importing the list of modules, registering them, and making them visible. We could then teach the customizer & build system etc to generate that list and wire it up for our "out of the box" users.
Thoughts? I have no conclusions at this point, only the high level goals, ideas, and more questions. ;) But if we all keep noodling on this I think we can get to a good solution.
From what I know from working with SystemJS all of this should be possible. It should be possible to create bundles for every module that uses all outside dependencies from global. Than dropping in the script files in the right order should work. But I guess the best would be to say. There is a package with all stuff in it and that's it. jQuery also doesn't offer just some parts and I guess most people only use 10% of jQuery but alway take it all.
When building a bundle with SystemJS you start with an entry file and only what is exported there is exposed. So for example when we build any modules we often just expose an init or bootstrap function and that's all that is accessible from the outside.
@kball Y'alls might need to dig in to whatever data you have on usage of the various flavors of Foundation.
I think trading individual package bloat for simpler standalone packages may be worthwhile, though. Consider that network overhead for multiple requests tends to be a greater bottleneck than file size, especially on high-latency networks. And we are mobile-first, y'all.
With that in mind, if you're using individual <script>
tags for JavaScript libraries in production code, then package size probably isn't a concern of yours either. If you're a performance nut then you're going to compile your scripts together and ensure you have no dead code.
With that assumption, then we can roll with the approach of compiling multiple UMD bundles, one for Foundation core and one for each plugin. Each file would contain one plugin and the necessary utility libraries for that plugin.
Now, regarding how to auto-bootstrap the components... from what I can tell, if we want every file to be side-effect free, then having components add themselves to a global registry isn't possible. This is because a reference to the external registry must exist, and the act of adding to the registry is a side-effect.
However, to Kevin's point, having the components auto-bootstrap is useful as a developer convenience. With that in mind, what we could do is create a component registry only if a plugin is being used in a browser global situation. So uh, basically keep the existing system. But if you're importing the plugins using a module bundler, they're manually initialized by you.
Last thing, it's worth considering what we want to be the public API of the framework. Once we have this whole system set up, the utility functions will be hidden behind the plugins, and won't be accessible to the end user unless we choose to expose them at the top level.
Once we make this shift, it will be very easy to refactor and shift around the internals of the framework as we please, as long as the public API and the build artifacts don't significantly change. So, if only the plugins are considered part of the public API, then those utility functions can be changed whenever. Their names, file locations, APIs, etc. would not matter to the end user, because they'd just get baked into the final build artifacts.
Before the launch of Foundation 6, we were wondering if the various utility libraries could be touted as a "feature" of the framework, but I don't know if that went anywhere.
As highlighted in https://github.com/zurb/foundation-sites/issues/8659 (and I believe in some others) the current in-line evaluation approach does not play nicely with async loading. This rework should make sure to take this case into account.
At this point, I'm leaning towards keeping this work separate from any potential Typescript move, because while I believe they are complementary, I also think they are mostly orthogonal. Auditing the current state of the JavaScript, I think we can tackle this in a few phases.
To this point, I think these are all non-breaking changes, not requiring any sort of API changes. I think these can be done in pieces, will be valuable refactoring, and can actually be merged along the way. Next is when it gets fun.
These are breaking changes, and likely cannot be done piecemeal. These will need to be done in a branch, probably by one person. Likely me, though if there's other volunteers, I'm happy to work with you. ;)
I don't have any detailed pieces here yet, only ideas, but I think encapsulating things fully will let us do all sorts of good stuff, such as the view layer separation discussed above, better handling of async initialization, etc.
I am +1 on typescript
Specially with foundation 7... Coz there still around a year according to roadmap for implementing big development changes
@kball I can help you out with the Webpack stuff if you'd like.
I'm in process with the webpack stuff, and running into the following question: How do we want to handle registering/deregistering instances of plugins/javascript enabled components?
Right now, inside of the constructor we always call Foundation.registerPlugin
, and in the destroy we call Foundation.unregisterPlugin
, off of the global namespace.
My current inclination is to maintain the concept of a global registry (e.g. an "app" context), and pass in the global Foundation
object on init... So our function signature for the constructor would change from constructor(element, options)
to constructor(Foundation, element, options)
To maintain backwards compatibility, we could actually have a part of the registration of the Plugin Class bind the Foundation object to the constructor, so that if you were accessing it via new Foundation.PluginName
the function sig would remain the same,
e.g.
Foundation.plugin = function(plugin, name) {
...
this._plugins[attrName] = this[className] = plugin.bind({}, this);
}
...
import Reveal from 'foundation.reveal';
Foundation.plugin(Reveal, 'Reveal');
var reveal = new Foundation.Reveal($('#reveal'), {});
var reveal2 = new Reveal(Foundation, $('#reveal2'), {});
@gakimball what do you think?
I don't like this
var reveal2 = new Reveal(Foundation, $('#reveal2'), {});
passing Foundation every time creating a new instance of a plugin seams a bit much. And in the end in TypeScript we should have the possibility to use it like this
import {Reveal, RevealOptions} from "foundation-sites";
const reveal = new Reveal(element: HTMLElement, options: RevealOptions);
the plugin way, I guess it should look like this
import {getPlugin} from "foundation-sites";
const foundationReveal = getPlugin("Reveal"); // which than would return the Reveal Constructor
const reveal = new foundationReveal(element, options);
@DaSchTour I agree it's ugly... the question is, how do we maintain a "global registry" of instantiated plugins... or do we need to?
Actually, digging into what Foundation core currently does with that global registry, it's minimal. The register/unregister appears to essentially be there to insert a standardized lifecycle. We do stash away the uuids in a global registry, but that stash is essentially unused.
We could probably get the same functionality simply using inheritance. E.g. have everything inherit from a Foundation.Plugin class that provides the basic lifecycle, then specify what you need to define (which we already do). The details of what you need to define would change because we'd want the base class to define the constructor, but other than that we could probably get back all of the behavior we're currently implementing without requiring a global state tracker or passing around the Foundation object.
Then if we decide we want to implement a global registry for some other reason, we could do so but make it "opt in" in some sense, using the same plugin wrapping approach...
Thoughts?
Following the approach in my above comment appears to be mostly working... the one place still that needs to be figured out is how we handle things like the responsive menu and responsive accordion tabs.
In current (6.3) approach, these look at parse time at the Foundation._plugins object and see which plugins are available. (e.g. do I have all of dropdown, drilldown, and accordion available? Or just 2?)
It seems like instead, we should make this configurable at instantiation time, but I'm not sure the right way to do so.
I see a few options, none of which I like too well...
Pass in the global Foundation
object on instantiation. This is the "simplest" answer in some ways because we can then look at the _plugins list then... the downside is that makes the function signature for these plugins fundamentally different than all other plugins.
Explicitly pass in the plugins we're planning to use, either as options or in a 3rd argument (NOTE in this I mean not the names, but the actual class objects already imported). We could do this "automatically" when instantiating through the $.foundation
method and thus having access to Foundation
. Bonus of this approach is it potentially allows savvy users to pass in their own classes of different types of menu plugin without any additional configuration.
require all possible plugins in these responsive files. E.g. if you're using responsive menus, you will have to have the JS for dropdown, drilldown, and accordion even if you don't use them. For responsive Accordion/Tabs this doesn't seem like a problem (because you wouldn't use the plugin if you didn't want both) but for the responsive nav it does force you to potentially include more JS than you need.
Right now I'm leaning towards using option 3 for simplicity of implementation, but I'm not fully convinced it's the right approach. Thoughts @DaSchTour and @gakimball?
@kball Just from reading option 3 sounds the best. Hopefully I'll find some time in the next days to create a sample of foundation JS migrated to TS to get an own impression of this topic.
@kball I like how option #2 sounds, although I wonder if it creates too much overhead in a CommonJS environment. In the browser global environment, you can scan window.Foundation
for plugins and set a default option for the Responsive Menu plugin. However, in a CommonJS environment, you'd most likely need to pass in the plugins you're using manually, like so:
import { ResponsiveMenu, DropdownMenu, DrilldownMenu } from 'foundation';
ResponsiveMenu.DEFAULTS.plugins = [DropdownMenu, DrilldownMenu];
The responsive menu is already kind of an advanced feature, so this overhead might be okay? But if people think it isn't, that's totally understandable.
Might also depend on what we know about how developers mix the various menu styles. The way the plugin was originally conceived, accordion and drilldown were the mobile-friendly styles, and dropdown was the desktop-friendly styles. That means in most cases your navigation solution is using exactly two of the three styles. You'd only need all three plugins if you happened to have multiple navs implemented in your design.
Edit: If not #2, I agree #3 is a good choice as well.
I think I'm going with #3 as a first pass due to the fact that is the simplest to implement... #2 becomes much more interesting if we extend the responsive menu plugin to allow user defined menus, or we end up with more forms of menu.
So I've got a pass on Phase 2 working (building all of Foundation with webpack) in this branch: https://github.com/zurb/foundation-sites/tree/move-to-webpack
Will be working on the individual distfile approach soon, but would love feedback on approach. @gakimball you mentioned you could help with webpack, can you take a look?
Ok... I have the individual distfiles building, also in that move-to-webpack
branch. The non-minified version of all of the distfiles adds about a 34% overhead relative to the non-module based approach from babel. I suspect this will drop with minification (the webpack boilerplate around the module itself is pretty verbose when dealing with these very small modules; I think the names probably minify ok though.)
The full "everything" package is actually smaller than our current everything package, though not by much.
The remaining tasks are
Development bonus of this approach - on my machine, full javascript recompiles take about 550ms total on this branch, as compared to 2-3 seconds in current develop. I suspect we'll be able to bring this into the templates as well and dramatically improve the build experience.
Hmm... so something that I'm struggling with (turned up by getting started reworking the customizer) is how to deal with the 2 utility modules we have that function by creating global listeners/side effects (triggers and touch). Both of these require initialization, and have some overhead so probably should be initialized as close to exactly once as possible (while they should be robust to reinit, I don't think we want every component initializing them in _init)
I think every foundation plugin depends on triggers, so it kind of seems reasonable have those be required in by core and initialized there.
For touch, I'm really not sure the right approach; not everything has touch controls, and it seems plausible that you'd want to exclude it... however, it functions by setting up both a global jquery special event and a global jquery function for adding touch. The latter seems like it could be legitimately required in by the elements that care, but the former is, well, global. Should we put this into the core setup as well? E.g. you can't get a version of foundation that doesn't have the touch handler? Or does anyone have a better idea?
@colin-marshall @gakimball @DaSchTour @Owlbertz
Well in general I would suggest to wrap this "services" into a promise and init if not already done and then resolve by returning this and if already initialized resolve immediately. So this utilities will only me initialized if needed.
If touch is coming as an issue on removing jquery as dependancy ..... Then why not something like hammer.js ? Hammer.js is fully supported by all the javascript frameworks out there ( angular, react, vue, you name it and surely even jquery )
@DaSchTour I like that idea; that would be a new pattern in Foundation, but I think a valuable one. Do you have an example that you think is particularly well done that you can point me to?
@kball well not really sorry. I've seen something in this direction in a project I work on. But it was done with observables and angular 2 services. And the usage of observables in this places wasn't a good choice I think. But in general it may look like this.
class Service {
private isReady: boolean;
private init() {
/* do init */
this.isReady = true;
return this;
}
public get() {
return new Promise((resolve) => {
if(this.isReady) {
resolve(this);
} else {
resolve(this.init());
}
});
}
}
Just jumping in here to throw rollup.js in to the convo as a possible alternative to Webpack. Webpack is still the dominant new-hotness in terms of bundlers but, considering V7 might take a while, it might warrant some consideration based on this thread. It looks like it's already being used by Vue, Ember, Preact, Riot, React, D3, PouchDB, Three.js, angular, inferno, and others. It's probably a good idea to keep an eye on this comparison chart on the Webpack site; With the backing it's getting, we'll probably see a number of those features fill up in the coming months.
Well coincidentally just out from bundling video of both rollup and webpack by @JeffreyWay https://laracasts.com/series/es6-cliffsnotes
and i also liked rollup more ... specially with buble! But that comparison chart is very low down ..... so much boots to fill!
Good call. JW Rocks and that series is great (and free if you don't have a sub). Definitely recommend watching.
Hmmn @gpspake .... but this advancement is no1 on the task list for 6.4 and in may-june infact https://github.com/zurb/foundation-sites/wiki/Project-Roadmap#64-tentative-date-may
So i don't see rollup very soon though buddy!
@gpspake before using rollup.js just use JSPM which is build ontop of rollup.js as I understood ist. But more important just some thoughts about Webpack. As I've seen Webpack add's additional syntax to SCSS and even needs some special behavior when using TypeScript. That's the reason why webpack failed in my search for a bundler, because the development ist determined by the bundler. Migration to another bundler would mean to rewrite the code, which is a total no-go for me. The code should be independent from the build process.
@kball i think you should poll again coz i am pretty sure.... typescript will be more adopted now Moreover whoever is polling in the way of ES6 is somewhat arguably polling in favour of typescript!
I also think that we should look to switch to typescript! Moreover Typescript is supported very well in all these three top JS Frameworks
I tend to concur with @DaSchTour and i think this is the right time to switch to Typescript!
Hey folks - I fell behind on this project for a while due to other projects, but digging into it again this week to get it back on track for the 6.4 release. A couple notes:
The majority of the work involved in this is independent of webpack or module bundler of choice, but is really around refactoring the modules to work in a bundler environment AT ALL.
I'm using webpack for our internal build work in this branch to generate distfiles, and given we are migrating many of our projects to webpack we will probably update the ZURB templates to webpack, but that should not prevent using a different bundler for your own purposes.
Moving to typescript is definitely still an option, but not for 6.4. If someone wanted to take a stab at looking at what this would look like and start exploring what architectural changes we'd need I'd be interested in discussing further..
I've taken a short look into this and started a branch to try some TypeScript refactoring. https://github.com/zurb/foundation-sites/compare/develop...DaSchTour:typescript It works quite well and it looks like it could work quite well without even breaking backward compatibility.
Interesting... it actually looks very similar in approach to the ES2016 WIP I have... I just rebased relative to develop here: https://github.com/zurb/foundation-sites/compare/develop...move-to-webpack. If you skip past all of the parts focused on generating distfiles into the actual source changes, there's a great deal of similarity.
@DaSchTour with the typescript approach, can we generate the "drop in" approach for legacy users without a buildsystem in a simpler way?
@kball I'm not quite sure about that. But I think that it will not be that easy. Will have to check and try.
@DaSchTour k... I'm going to keep plowing forward with the ES2016 approach for now, but especially given how similar it looks I think moving to TypeScript for e.g. 6.5 might make sense.
@kball just my 2 cents, but I really don't like default exports. You can do a lot of strange things with default exports, something like import Reveal from "./foundation.accordion";
so I would suggest to use named exports. Especially when defining Interfaces together with a class this makes the import a lot nicer.
We want to bring Foundation's JavaScript dependency management up to 2016.
Some challenges will include:
However, I think the benefits will far outweigh the costs. Among other items, this should fix https://github.com/zurb/foundation-sites/issues/7386 and make it way easier to integrate Foundation components into other systems. It will also lay the groundwork for easier refactoring of Foundation down the road (jQuery removal, I'm looking at you!)