ractivejs / ractive

Next-generation DOM manipulation
http://ractive.js.org
MIT License
5.94k stars 396 forks source link

New feature: import components from HTML file #366

Closed Rich-Harris closed 10 years ago

Rich-Harris commented 10 years ago

This probably isn't fully baked yet, but it's a really neat idea (suggested by @davidmoshal in #299), and I'd be interested in feedback.

The idea is to be able to fully define components in a single file that contains the template, any CSS, and custom methods/init logic. It draws inspiration from Web Components and HTML Imports. If we get it right, it will make it really easy to create reusable components that can be shared widely (think Ractive UI, like jQuery UI or Angular UI, except not an unloved cousin to the main project :)

Making a component

You define a component in an HTML file (because HTML can contain <style> and <script> tags, and text editors are happy to chop and change syntax in that situation - much better than the nightmare that is multiline string templates in a .js file).

<div class='box' style='width: {{size}}px; height: {{size}}px'>
  <p>This box has a total area of {{ format(size*size) }}px.</p>
</div>

<!-- This CSS will be appended to the page when the first instance
     of this component is created, and removed when the last instance
     is torn down. It is optional. There's no scoping magic, we have to
     just be careful with class names -->
<style>
  .box {
    background-color: black;
    color: white;
  }
</style>

<!-- The script block is optional (as is the style block) -->
<script>
  module.exports = function ( Ractive ) {
    return Ractive.extend({
      init: function () {
        console.log( 'initialising box!' );
      },
      data: {
        size: 200,

        // add comma separators
        format: function ( num ) {
          var remaining = '' + num, result = '', lastThree;

          while ( remaining.length > 3 ) {
            lastThree = remaining.substr( -3 );
            result = ( ',' + lastThree + result );

            remaining = remaining.substr( 0, remaining.length - 3 );
          }

          return remaining + result;
        }
      }
    });
  };
</script>

Loading the component

Then you load the external file in one of two ways:

1. As a globally available component

<!doctype html>
<html>
  <head>
    <title>Component imports test page</title>

    <!-- here is where we import the component. We could add a 'name'
         attribute, otherwise it will default to Ractive.components.box -->
    <link rel='ractive' href='box.html'>
  </head>

  <body>
    <div id='container'></div>

    <script src='Ractive.js'></script>
    <script>
      var box;

      Ractive.load().then( function () {
        box = new Ractive.components.box({ el: 'container' });
      });
    </script>
  </body>
</html>

2. Loading a specific component

<!doctype html>
<html>
  <head>
    <title>Component imports test page</title>
  </head>

  <body>
    <div id='container'></div>

    <script src='Ractive.js'></script>
    <script>
      var box;

      Ractive.load( 'box.html' ).then( function ( Box ) {
        box = new Box({ el: 'container' });
      });
    </script>
  </body>
</html>

The 'box.html' path will be resolved relative to Ractive.baseUrl, if it exists (so you could put all your components in a components or imports folder without having to explicitly refer to that folder each time).

Dependencies

Some components might have dependencies on other components. One way to handle that is to have separate <link> tags for them all, so that all components are globally available. Another is to use the @import directive:

@import foo.html
@import ../bar.html
@import somethingElse.html as baz

<div class='component-zoo'>
  <h2>This is a foo</h2>
  <foo/>

  <h2>This is a bar</h2>
  <bar/>

  <h2>This is a baz</h2>
  <baz/>
</div>

Cross-origin stuff

The html files have to be hosted on the same origin, or served with appropriate CORS headers.

Optimisation

Clearly this is isn't a great way to build large apps, because you don't want to have to make a ton of requests. At some point in the future there'll be a build tool that will compile components (and their dependencies) into a JavaScript bundle, that would work with grunt et al - the Ractive AMD loader plugin will also serve this purpose (when used with the RequireJS optimiser). Uncompiled component imports won't work with the runtime builds, as they need to be parsed.

Promises (an aside)

Under the hood, this is using promises. (If any promises experts out there can see any ways in which this implementation deviates from the spec, let me know!) I'm tempted, now that they're in the library, to start using them in other places (e.g. returning a promise from ractive.set() and ractive.animate() etc, rather than using callbacks and making them chainable)... any thoughts?

How to start testing them

Grab the latest dev build from https://github.com/RactiveJS/Ractive/tree/0.4.0/build.

Just to clarify - nothing is set in stone here - the Ractive.load() method name, the <link> approach, the component format, the @import directive - it's all just fodder for discussion at this point. If you have better ideas please share them!

ghost commented 10 years ago

Hey, I'm very new to Ractive, but I really like the concept. However I wouldn't use the @import in the component code. Every component is basically a html file, so I think the <link> is the most appropriate solution there, too. So every component could include other components the same way as the parent html.

marcello3d commented 10 years ago

Why not use browserify to require and bundle modules? It's perfect for this kind of thing and pretty well battle-tested. I'm already using it to inline compiled ractive templates in my javascript on the server-side (as simple as require('./path/to/template.ract')).

davidmoshal commented 10 years ago

Nice Rich! Personally, I prefer Browserify over RequireJS btw. Dave

On Tue, Jan 7, 2014 at 8:07 AM, Rich Harris notifications@github.comwrote:

This probably isn't fully baked yet, but it's a really neat idea (suggested by @davidmoshal https://github.com/davidmoshal in #299https://github.com/RactiveJS/Ractive/issues/299), and I'd be interested in feedback.

The idea is to be able to fully define components in a single file that contains the template, any CSS, and custom methods/init logic. It draws inspiration from Web Components and HTML Importshttp://www.html5rocks.com/en/tutorials/webcomponents/imports/. If we get it right, it will make it really easy to create reusable components that can be shared widely (think Ractive UI, like jQuery UI or Angular UI, except not an unloved cousin to the main project :) Making a component

You define a component in an HTML file (because HTML can contain

Loading the component

Then you load the external file in one of two ways:

  1. As a globally available component

<!doctype html>

Component imports test page ``` ```
``` ``` 1. Loading a specific component Component imports test page
``` ``` The 'box.html' path will be resolved relative to Ractive.baseUrl, if it exists (so you could put all your components in a components or importsfolder without having to explicitly refer to that folder each time). Dependencies Some components might have dependencies on other components. One way to handle that is to have separate tags for them all, so that all components are globally available. Another is to use the @importdirective: @import foo.html @import ../bar.html @import somethingElse.html as baz

This is a foo

This is a bar

This is a baz

Cross-origin stuff The html files have to be hosted on the same origin, or served with appropriate CORS headers. Optimisation Clearly this is isn't a great way to build large apps, because you don't want to have to make a ton of requests. At some point in the future there'll be a build tool that will compile components (and their dependencies) into a JavaScript bundle, that would work with grunt et al - the Ractive AMD loader pluginhttps://github.com/RactiveJS/requirejs-ractive/will also serve this purpose (when used with the RequireJS optimiser). Uncompiled component imports won't work with the runtime builds, as they need to be parsed. Promises (an aside) Under the hood, this is using promises. (If any promises experts out there can see any ways in which this implementationhttps://github.com/RactiveJS/Ractive/blob/0.4.0/src/utils/promise.jsdeviates from the spec http://promises-aplus.github.io/promises-spec/, let me know!) I'm tempted, now that they're in the library, to start using them in other places (e.g. returning a promise from ractive.set() and ractive.animate()etc, rather than using callbacks and making them chainable)... any thoughts? How to start testing them Grab the latest dev build from https://github.com/RactiveJS/Ractive/tree/0.4.0/build. — Reply to this email directly or view it on GitHubhttps://github.com/RactiveJS/Ractive/issues/366 .
monolithed commented 10 years ago

+1 Only it should be a plugin

browniefed commented 10 years ago

I really like this however I imagine for people using SASS/Compass there would likely be a build step to ultimately compile the web components HTML/JS/Css together.

Rich-Harris commented 10 years ago

Thanks for the feedback so far everyone.

codler commented 10 years ago

@Rich-Harris you are not alone, I have not tried browserify, grunt, require etc :P They feel so new, new big things popups every month.

martypdx commented 10 years ago

My first reaction was to use a json type package specification. Are you familiar with https://github.com/component/component?

I hadn't read up on the HTML Imports yet, thanks for the link. I see why you might lean to using the link tag.

What is the sweet spot for Ractive? Might you be better off fitting into component or bower or ...? Or leveraging the Polymer polyfill?

Rich-Harris commented 10 years ago

@martypdx Having a ractive.json (or equivalent) is something I'd rather avoid, truth be told, and not just because me and @codler are grumpy old men!

Warning: Long-winded post ahead.

One of the design goals for Ractive (which I should really get around to writing down at some point) is to be as agnostic as possible about ecosystem. So you can use it by having a good old fashioned <script src='lib/Ractive.js'></script> tag, or as an AMD module, or with Browserify, or in node, and you can include it in a project via bower (bower install ractive) or indeed component (component install RactiveJS/Ractive). I don't use all of those things, but other people do, so they should be supported. So creating a new package/dependency management system and expecting people to use it alongside their existing setup would feel contrary to that philosophy.

Another design goal is that Ractive needs to be easy. I don't mean easy as in 'it's easy, you just npm install -g ractive then ractive install datepicker-widget, then ractive bundle -o js/lib/Ractive.js and then you can include it in your project', I mean easy as in, for people who have no clue what npm and 'build processes' are, but can do basic HTML and JavaScript. That's not to say that it should be simplistic or dumbed-down, just that we as a front end community often aren't vigilant enough about barriers to entry - those systems seem straightforward to us (the initiated), but for newcomers they're deeply imposing. I've written a bit about the project's origins and who it was originally aimed at, which might help explain where I'm coming from: http://blog.ractivejs.org/posts/the-origins-of-ractive.

Component is an interesting project I don't really understand. It seems (from reading about it) like it's focused on UI components, but when I go to http://component.io/ I mostly see a bunch of AJAX helpers and so on. So it's... just another module system, but one that allows you to bundle HTML and CSS? There's no 'about' page, so it's genuinely a little hard to tell what it's for. As a newcomer I'd be bewildered. I don't want to criticise the project without better understanding it (I don't know TJ, but I know he's a far more accomplished developer than me, for starters), but I don't think it solves the problem of creating reusable, shareable, and flexible UI components, because it forces you to use its ecosystem. (You can bundle components for non-component users with --standalone, but I assume the result isn't human-readable?)

In the world I see (apologies to Tyler Durden), UI components are distributed as HTML files in gists, and emails, and code blocks in blogs and tutorials. Novice programmers can copy and paste them into their projects and immediately grasp what they're doing and how to interact with them because it's speaking languages they already know, without having to trawl through API documentation that someone has to painstakingly create and maintain. And then they can start customising it by tweaking the CSS, and playing with the markup, and then they realise that they can create reusable components too and share them with other people. A thousand components bloom. The best examples of UI and interaction design are copied; the web becomes a better place. Meanwhile more experienced developers can incorporate the components into their apps via their grunt/gulp/npm run/browserify/whatever build process. And people can go window shopping for components at places like ui.ractivejs.org, and there's no extra step to create a demo/sandbox page because there's a standard in place - it happens automatically.

At the moment none of that exists, but it could!

copongcopong commented 10 years ago

@Rich-Harris I'd personally go with using the same syntax as how we do inline partials, to divide html fragment, style, and onComplete callback scripts.

This was way, it adhere closely with RactiveJS' templating logic. Probably a specific namespaced-like-partial-comment-declaration.

<!-- {{ >component:style }} -->
<!-- {{ >component:callback }} -->
<!-- {{ >component:data }} -->
<!-- {{ >component:template }} -->
davidmoshal commented 10 years ago

Rich, couple of thoughts:

a) Regarding inter-component data api:

I'm imagining data is exchanged by ractive's ViewModels (ie: 'set' and 'get'), using ractive's eventing to signal consumers that data is ready to be 'gotten' (if that's the right verb). Naturally ES6 'magic' variables could be used here too.

In my opinion you should be able to understand the data exposed and consumed by any component by simply looking at the {{ }} 's.

Direct manipulation of the View by dom id's, (other than by other View components) should be discouraged.

(after all, the distinguishing feature of the reactive MVVM pattern is that neither the Model nor the ViewModel hold references to the View, and the easiest way to avoid that is to avoid the use of dom ids, longer discussion I guess).

b) Regarding component composibility:

In your examples you show clearly how an embedding component can embed another component without needing access to that (embedded) component's internals.

However, you might want to consider the converse, in which the embedded component doesn't need to know the internal of the embedding component.

Take the canonical example of a layout decorator component: how do you imagine that you could treat the Layout component as a black box component, and pass in a Navbar component, for example?

Currently I handle this by first loading the embedding component (the Layout component), then in its complete handler, attaching the embedded component (the Navbar) by looking up the dom id. (as this is one View referencing another View, I don't object to using id references).

However, I imagine that your could somehow pass the embedded component in via triple {{{ }}} , or other mechanism.

It's a pity that HTML is has so little support for templating. In Jade for example, one can compose like this.

Your thoughts?

Dave

On Wed, Jan 8, 2014 at 4:39 PM, copongcopong notifications@github.comwrote:

@Rich-Harris https://github.com/Rich-Harris I'd personally go with using the same syntax as how we do inline partials, to divide html fragment, style, and onComplete callback scripts.

This was way, it adhere closely with RactiveJS' templating logic. Probably a specific namespaced-like-partial-comment-declaration.

— Reply to this email directly or view it on GitHubhttps://github.com/RactiveJS/Ractive/issues/366#issuecomment-31892220 .

davidmoshal commented 10 years ago

@Rich-Harris I spent some time with component.io yesterday.

Despite being a TJ fan (I use express and the excellent jade template library extensively), however, I'm underwhelmed.

Firstly, there's the issue of documentation: a search for "component io" leads to a page listing hundreds of plugins, but no documentation. A search for "component js" leads to a very well documented, though completely different, project called componentjs. Eventually I found the documentation at: https://github.com/component/component It has links to 2 screencasts and a few articles (some of those links are broken). So, not a good initial impression.

The way it works is as follows:

  1. you create (or have component created for you) a directory with a component.json file.
  2. that component.json file contains a list of all of your javascript, html, and css files
  3. additionally the component.json file can list dependencies on other components, and paths to find local components.
  4. the command 'component install' will then fetch the dependencies for you.
  5. the command 'component build' will created 2 files for you:

    a) a 'bundle.js' file containing all of your javascript files, and your html files, (which have been converted to large strings embedded in javascript).

    b) a 'bundle.css' file with all of your style sheets

you can these use these 2 files an html file to load your entire application.

So far so good, but now the problems:

a) coffeescript: despite 2 plugins, coffeescript script support doesn't work. You'd have to find another mechanism to convert to js first, then it will work. This is really aimed at vanilla js and css, though any html template library can be used I think.

b) level of granularity: this component system is really aimed at macro, standalone, components. In my case, I wanted to create a separate module out of each of my applications sub-parts (user management, landing page, auction management, bidder page, etc.), but this could only be done by treating each sub-component as a regular component, ie: adding all of the application dependencies to each sub-component.

So, for example you'd have multiple copies of Ractive.js, jquery, bootstrap, etc, distributed around your application, one of each for each sub-component). Maintaining these would then become a nightmare.

In summary, component.io is useful for bundling and distributing the js, html and css of an ENTIRE application or macro component. Not good at all for managing the sub-components of a single application. ie: wrong level of abstraction for us, in my view, it's more of a module management system than a component management system.

David

davidmoshal commented 10 years ago

@Rich: tried out build 4.0, both the link and the load method work brilliantly. I wasn't able to get @import to work, not sure what it adds though, so not bothered. Note: neither link nor load work on IE8, even with Ractive-legacy.js is there some sort of polyfill/shim I need?

davidmoshal commented 10 years ago

Seems like parsing errors disappear, for example: I tried this .

<table>
    {{#users}}
    <tr><td><div>username: {{ username }}</div></td></tr>
    {{#users}}
</table>

The second {{#users}} should be {{/users}}, in which case it works.
No error generated though, so quite difficult to track down.
davidmoshal commented 10 years ago

So, current issues with this feature: 1) not working in IE8 even with Ractive-legacy. 2) parsing errors disappear (not sure where they should go).

rvangundy commented 10 years ago

Hey, just piping in on this library. Like what I see! I've been thinking about spec'ing a component solution for a while. My preference is for a UI component to be placed in its own folder with its HTML, JS, and CSS, (I use SASS+Handlebars) like:

myComponent
|-- style.sass
|-- index.js
|-- template.hbs

With tools like browserify, you only need to require('./src/myComponent'), or, if loaded with NPM, require('myComponent') and the index.js file is inferred. I feel pretty strongly that UI components should be pulled in during a build process that includes the compilation of the CSS, template, etc. (with browserify, there ought to be a component-consuming transform that adds CSS to a CSS precompiler list). Components can be published on package management systems using standard bower.json, package.json, etc.

I've noticed a general sentiment that RactiveJS should be user-friendly. Package management and build processes are critical for code development in the JS world now. It isn't that they aren't user-friendly so much as there's no one way to do it--so new users tend to just include a bunch of <script> tags in <head>. If you're talking about things like including another AMD solution within RactiveJS, I'd question whether this is to encourage user-friendliness or to work around the lack of a more standard build/module-loading solution--in other words, throwing another solution on the pile. I'd encourage instead making RactiveJS more user-friendly by creating a good project initializer or Yeoman generator that gets them off on the right foot with the right tooling, and focus on RactiveJS as a lean-and-mean reactive DOM manipulation solution.

Anyways, truly amazing work so far on this project. Sorry for so much opinionated posting, been philosophizing on some of these points for a while.

Rich-Harris commented 10 years ago

Okay, an update:

Components importing other components

The @import foo.html thing has been replaced with <link rel='ractive' href='foo.html'> - less compact, but more consistent (both internally and with the HTML imports way of doing things). Thanks @netlovers for making this suggestion.

Change to exports API

If a component has a <script> block, instead of module.exports we have component.exports - that way we're not pretending we're in a node-y environment, and users are less likely to try and do something node-y like require('foo') or process.nextTick() and confuse themselves in the process.

There's also a shorter syntax - before, you had to export a factory function, now you can just export an object:

<button on-click='activate'>Activate</button>

<script>
  component.exports = function ( Ractive ) {
    return Ractive.extend({
      init: function () {
        this.on( 'activate', function () {
          alert( 'Activating!' );
        });
      }
    });
  };
</script>

...is equivalent to the shorter syntax...

<button on-click='activate'>Activate</button>

<script>
  component.exports = {
    init: function () {
      this.on( 'activate', function () {
        alert( 'Activating!' );
      });
    }
  };
</script>

Syntax

Been thinking about @copongcopong's proposal. The major disadvantage as I see it is that we lose something precious: by using HTML as the container language, and just requiring authors to add <style> and <script> tags if they want to include default styles and default data/behaviours, we a) don't add anything new to learn, and b) - the biggie - we get syntax highlighting for HTML, CSS and JavaScript for free, both in text editors but also in code blocks and gists and what-have-you.

Encapsulation & composability etc

Responding to @davidmoshal's points:

In my opinion you should be able to understand the data exposed and consumed by any component by simply looking at the {{ }} 's.

Yes, definitely. In cases where it's not obvious it would make sense for component authors to include an example at the top of the file:

<!--
  GitHub cards, courtesy of http://lab.lepture.com/github-cards/

  Usage:  <github-card user='RactiveJS' repo='Ractive'/>
    or... <github-card user='RactiveJS'/>
-->

<iframe
  src='http://lab.lepture.com/github-cards/card.html?user={{user}}{{#repo}}&amp;repo={{repo}}{{/repo}}'
  frameborder='0' scrolling='0' width='400' height='200' allowtransparency
></iframe>

In case it's not obvious, the component's [view]model is generated from the attributes. So in the top usage example above, the component would be created with data: {user: 'RactiveJS', repo: 'Ractive'}. Nice and straightforward, and prevents components from accessing data that doesn't belong to them. However it does mean occasionally doing things like <widget foo='{{foo}}'/>, where it might be better if the foo reference was passed down implicitly - my current thinking is that this should work a bit like JavaScript's lexical scoping, where you keep going up the scope chain until you can resolve a reference.

Direct manipulation of the View by dom id's, (other than by other View components) should be discouraged.

Agree again. While there's not much you can do to prevent outside forces manipulating DOM that belongs to a component (until Shadow DOM is widely supported at least), it's fairly straightforward to avoid IDs and encapsulate DOM references with e.g. input = this.find('input').

how do you imagine that you could treat the Layout component as a black box component, and pass in a Navbar component, for example?

I think this is an absolute must for components. It's not a trivial problem but I'd like to at least make a start on it for the 0.4.0 release. Your current approach is probably the best solution given the way things currently work.

this component system is really aimed at macro, standalone, components

Thanks for sharing your research on this. I think that's the key insight - the only way to achieve true encapsulation is to make no assumptions, and it's expensive not to make assumptions (e.g. that jQuery is already on the page). There's a bit of an irony here, that a system designed to increase modularity ends up making things less modular.

The advantage of these being Ractive components (ecosystem-agnostic but library-dependent), rather than components in the component.io sense (library-agnostic but ecosystem-dependent), is that the most important assumption has already been made. What isn't addressed is how we account for other dependencies - Ractive plugins, and 3rd party libraries - without falling into the same traps. That'll be the hard part.

neither link nor load work on IE8

Ah, I was using document.head. Changed it to document.getElementsByTagName('head')[0]. Can't check that it works because my laptop's in for repairs, and the missus wouldn't appreciate me installing VirtualBox on hers... but fingers crossed.

As for the parsing errors, the exception is probably being swallowed up by the promise - if you pass an error handler as the second argument to .then() you should see it:

Ractive.load().then( function () {
  // success!
}, function ( err ) {
  console.error( err.message || err );
});

Package management

@rvangundy Don't apologise, this is exactly the right venue to be having this conversation! We definitely agree that you can't really develop serious apps without a build process of some kind, and I'm all for encouraging developers down that path. The trouble as I see it is two-fold:

  1. Best practice changes every week, and it's exhausting. (See e.g. gulp > grunt for a recent example). On the package manager front, we've talked about bower and component, and npm isn't going anywhere, but what about jam, volo, ender? Each of them was the 'new hotness' (I despise that phrase...) at one point but each has (as far as I can tell) faded into obscurity. We'd be naive to imagine that it won't happen again. Which ones do we expect people to support? Meanwhile with module systems/build tools, anything that requires browserify is a non-starter in my view, not because browserify isn't good, but because not everyone wants to use it, and we shouldn't impose (potentially transient) technology choices on other people. Sure, we can distribute compiled components, but that largely defeats the object.
  2. Developers are profoundly lazy, or, at least, most energetic in the pursuit of laziness. With every additional stipulation (e.g. 'it needs a package.json'), we lose some proportion of people who would share their work. It's great if a component has thorough documentation, disciplined versioning, easy installation with my choice of package manager, etc - I'm more likely to use it. But I think it's healthier if component authors aren't forced to do all that stuff. (As well as the laziness aspect, I waited a long time before participating in open source software because of 'stage fright', which I think afflicts a lot of developers, and which I think is made worse by a lot of the ceremony in modern FED.)

Having said all that you make a very good point about the undesirability of creating yet-another-module-solution. That's absolutely something to avoid. So far, the Ractive.load() method doesn't add a huge amount of code (and I agree with @monolithed that perhaps it should be a plugin, rather than in the core library), though I'll be keeping an eye on it!

I'm also a SASS fan, so I'll be thinking about ways to support authoring in SASS as well.

Notes - still to do/think about

Experimenting

As ever, the latest dev builds are here. Thanks for the feedback!

davidmoshal commented 10 years ago

Thanks, lots to digest here. I will evaluate the latest build tomorrow (I'm on pacific time).

Regarding promises: personally I prefer asyncflow (https://github.com/sethyuan/asyncflow), it handles errors nicely, you might want to take a look at it.

Re: the build system, there doesn't appear to be a 'right' answer at this time. I like CommonJS/Browserify because I can use exactly the same files on both client and server (eg common viewmodels, 3rd party library abstractions), even though AMD/Jam offers more features (async loading, non-js files, minification, etc).

My vote would be to keep our options open on the build tool issue for now, and later find some way to bundle the components for production into one js file (with the js and html) and one css file.

davidmoshal commented 10 years ago

new syntax works very nicely, thank you. problems:

  1. still not rendering on IE 8,
  2. errors still failing silently, even when err parameter added to callbacks.
  3. 'complete' handler executes before 'init' handler, so, maybe 'init' should be relabeled 'render', 'instantiate', or 'create' ?
davidmoshal commented 10 years ago

Actually, it's not clear to me what the difference is between complete and init. It seems that init() is executed after complete() in all cases. Would it make sense to omit init() and just use complete() - that would avoid the ambiguity.

codler commented 10 years ago

I wonder how you would name the componant and will it be possible to have inheritance?

davidmoshal commented 10 years ago

Rich, I downloaded the latest 0.4.0 build (Jan 20), to see if the IE8 bug still exists, however, the legacy builds seem different from the other builds: they have 'define' functions. Is RequireJS now a requirement?

Alternatively, should we no longer be looking at the 0.4.0 brach for this feature?

btw: we've refactored an application to use this feature, and it's seems to significantly improve productivity, we're hoping to demonstrate to clients in 2 weeks (hence the need for IE8).

Thanks again, Dave

Rich-Harris commented 10 years ago

@codler At the moment, if a component is loaded via a <link> tag, it works like this:

<link rel='ractive' href='foo.html'>  <!-- name is foo, because href is foo.html -->
<link rel='ractive' href='bar.html' name='widget'>   <!-- name is bar -->

i.e. these would become Ractive.components.foo and Ractive.components.widget respectively. If instead you did

Ractive.load( 'foo.html' ).then( function ( Foo ) {
  // the component isn't exposed outside this function
});

Hope that makes sense.

@davidmoshal The difference between init and complete is a bit confusing I agree - basically, init is called as soon as the instance has been rendered, whereas complete is called once any intro transitions that are part of the initial render have been completed. Might rethink this a bit in future.

Going to look into that IE8 bug. Hopefully it's something obvious and not one of those tear-your-hair-out-cursing-Microsoft ones.

The define() thing - no, there's no requirement to use Require. AMD is used to organise the code internally - the build process uses the r.js optimiser to bundle everything into a single file, then a tool called amdclean removes all the AMD boilerplate so that it runs without a module loader. The legacy.js file used to be excluded from that process (it was simply concatenated to the main file for the legacy builds) until #402 - now, it's part of the library proper, and is stubbed out by the optimiser for the non-legacy builds. All a bit convoluted, but hopefully makes sense.

Glad you've found it beneficial to productivity - I've been dogfooding it at work and had the same experience. Bodes well.

davidmoshal commented 10 years ago

re init/complete: I'm just using complete for now.

re legacy: I'm still a bit vague, which file, of which build should I use for IE8? Should it be the Ractive.js of 0.4.0 branch?

Rich-Harris commented 10 years ago

I've extracted Ractive.load() into a plugin (thanks @monolithed for that suggestion), which lives in a separate repo:

An upcoming change I plan to implement: if component.exports is a function, then the first argument will be the base Ractive object. At the moment the argument that gets passed is the product of doing Ractive.extend({template: theTemplate}), which apart from being a little confusing means that you can't access properties like Ractive.svg or Ractive.Promise or whatever else.

It's also less hackable - you can't do things like Ractive.utils = {_:_} to use Underscore inside your component, or stuff like that. (I'm not sure if that's a good or a bad thing, but hackability is always good in itself in my view.)

@davidmoshal finally got a chance to look into that IE8 stuff. I was totally wrong before - I didn't think there was a define() call in the build, but that's because the build process was messed up. It's fixed now. Sorry. The demo page above works in IE8. 0.4.0 is probably the best branch to use, it has a few recent fixes for IE8 bugs. (It's like Pokemon - gotta catch 'em all...)

martypdx commented 10 years ago

An upcoming change I plan to implement: if component.exports is a function, then the first argument will be the base Ractive object.

That seems goofy IMHO. Why don't you just name the function with arguments and invoke it?

scriptElement.innerHTML = 'function ' + unique_name+' (component, Ractive) {' + script + '};';
var component = {};
head.appendChild(scriptElement);
window[unique_name](component, Ractive)

Or do you need to really append it to head?

var fn = new Function('component', 'Ractive', script);
var component = {};
fn(component, Ractive);
martypdx commented 10 years ago

Sorry, I didn't fully grasped what you meant until I reread it. I thought you were introducing a third variation, but this is just modifying the first long form of returning a function. I still like the mod I suggested above for the short form so you have access the Ractive.

How will the template be specified for the function return? Appended after the function returns?

One of the things I like about the current load is that you can omit parts you don't use. So if my component only has the template, I don't have to have a script.

Likewise, if my template is short (or dynamic) can I specify in the component extend and not have a template in the link html? (I haven't tried this in current code).

Rich-Harris commented 10 years ago

Finally got round to updating a couple of things.

1. The Ractive.load() plugin

Firstly, the 'function form' for exporting components has been deprecated. I prefer @martypdx's approach - you can now use the Ractive variable within the script block:

<!-- no longer supported... -->
<script>
  component.exports = function ( Ractive ) {
    return Ractive.extend({
      init: function () {
        // too much indentation!
      }
    };
  };
</script>

<!-- instead, do this: -->
<script>
  component.exports = {
    init: function () {
      // we can still use `Ractive`
      alert( 'Using Ractive version ' + Ractive.VERSION );
    }
  };
</script>

There's also a third 'pseudo-global' variable (the code is actually wrapped in an IIFE) alongside component and Ractive: require. The idea behind this is that you often need to use external libraries, and there should be a consistent interface for doing so that doesn't rely on the global namespace. The logic behind this will become more obvious in step 2 below.

So if you're using CodeMirror to build a <codemirror/> component:

<textarea></textarea>

<script>
  var CodeMirror = require( 'CodeMirror' );
  component.exports = {
    init: function () {
      var editor = CodeMirror.fromTextArea( this.find( 'textarea' ), {
        // options..
      });

      // set up some event handlers so that this component presents
      // a standard Ractive interface for observing/triggering data changes...
    }
  };
</script>

The Ractive.load() plugin will look in two places for the dependency: Ractive.lib.CodeMirror and (if it doesn't find it) window.CodeMirror. So you can add dependencies without polluting the global namespace, if you want.

2. rvc.js - an AMD loader plugin

I've also just released rvc.js, the Ractive component AMD loader plugin. If you're building an AMD project, you should use this instead of the Ractive.load() plugin.

It works with the Require.js optimiser, so if you're using that as part of your build process, your components will be optimised and inlined.

Here's where the require() thing starts to make sense: in an AMD context, foo = require('foo') tells Require.js (or whatever AMD loader you're using) to load foo first. And it works with whatever paths config you've got set up etc (basically it just delegates to the loader).

So a component can work with either the Ractive.load() plugin or AMD, loading dependencies with the consistent require() interface. The next step is to create a browserify transform. I'm not especially familiar with browserify, so if anyone wants to have a crack at this let me know!

@martypdx - yes, the style and script tags are still totally optional, and you could certainly create an inline 'subcomponent' with Ractive.extend() rather than importing it via a <link> tag, if you so chose.

marcello3d commented 10 years ago

Browserify transforms are pretty simple, they get a file name and contents, and can modify the content.

My ractify plugin checks if the filename ends in .ract, Ractive.parse()s it, and returns the resulting JSON.

This sounds like a simple step up from that. I'd expect the cleanest API would look like this:

Ractive.components[ 'github-card' ] = require('./path/to/github-card.ract');

If you could give me a example of what that literal generated JS should look like (e.g. what you'd hand-write in place of the require() command), and I can do the rest.

Having ractive files called .html is a little more hairy (and slightly disingenuous), hence my use of .ract. You could, however, have code to detect ractive in an html file rather than check the filename.

dfreedm commented 10 years ago

Sorry if this has been asked before, but have you looked at using an HTMLImports polyfill rather than roll your own? We've been cranking on https://github.com/Polymer/HTMLImports for a while now and follow the spec rather closely (only a few polyfill dependent tradeoffs).

I can see that you might consider having your own in order to tie more closely to your CSS encapsulation scheme, but it might be worth evaluating forward compatibility with the HTML Imports spec.

Rich-Harris commented 10 years ago

@marcello3d Awesome, thanks! I've created a gist with some sample output from the AMD loader: https://gist.github.com/Rich-Harris/9235461. A lot of the logic can be extracted from the rvc.js source fairly easily - I've added some links in the gist. Give me a shout if you need anything.

The file extension thing... I hear you on it being disingenuous. The main reason I used .html is that it means developers don't need to do anything to configure their text editors to get the correct syntax highlighting etc. I think that's quite a big advantage but I'm open to persuasion.

@azakus The spec outlined here is definitely inspired by HTML Imports, just like Ractive components are partly inspired by Web Components. But Ractive imports aren't designed to replace HTML Imports - they're narrowly focused on one specific job, and it's a job that couldn't be done by HTML Imports.

For example, the following code is a valid Ractive template (because it implements Mustache), but isn't valid HTML - so the DOM tree represented by link.import would be messed up:

<table>
  <tr><th>name</th><th>job</th></tr>
  {{#people}}
    <tr><td>{{name}}</td><td>{{job}}</td></tr>
  {{/people}}
</table>

Because of that, forward compatibility is an impossible design goal, so Ractive has to implement its own solution. It's not an NIH thing. I definitely appreciate the work that you and the other Polymer devs are doing on thinking through some of these problems - as I say, the provisional specs have been useful inspiration. Thanks!

marcello3d commented 10 years ago

Ok, here is my first attempt at component support. Seems to work, and the API is pretty clean: https://github.com/marcello3d/node-ractify/tree/components

Here's some sample input/output: https://gist.github.com/marcello3d/3584daf47580f96155f6

martypdx commented 10 years ago

Looking for a way to use an existing require. Basically, I bundle my component dependencies via browserify and then require in component load.

Can't do Ractive.lib = require because lib is a dictionary and require is a function.

Can't do anything here:

return function ractiveRequire ( name ) {
    var dependency, qualified;

    dependency = Ractive.lib[ name ] || window[ name ];

    if ( !dependency && typeof require !== 'undefined' ) {
        dependency = require(name);
    }

    if ( !dependency ) {
        qualified = !/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test( name ) ? '["' + name + '"]' : '.' + name;
        throw new Error( 'Ractive.load() error: Could not find dependency "' + name + '". It should be exposed as Ractive.lib' + qualified + ' or window' + qualified );
    }

    return dependency;
};

because require has been hijacked. Tried ractiveRequire.require = window.require but there's some recursive stuff with subcomponents and it works first level, but not second.

martypdx commented 10 years ago

fyi, doing this for now:

return function ractiveRequire ( name ) {
    var dependency, qualified;

    dependency = Ractive.lib[ name ] || window[ name ];

    if(!dependency && typeof Ractive.lib === 'function'){
        dependency = Ractive.lib(name);
    }

    if ( !dependency ) {
        qualified = !/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test( name ) ? '["' + name + '"]' : '.' + name;
        throw new Error( 'Ractive.load() error: Could not find dependency "' + name + '". It should be exposed as Ractive.lib' + qualified + ' or window' + qualified );
    }

    return dependency;
};
Rich-Harris commented 10 years ago

Closing this issue as this feature is now reasonably mature. Just to recap, you can author Ractive components in .html files, and load them with one of the following:

For the super curious, there's also rcu, which is a utility library for creating loader implementations (it's used in ractive-load and rvc). It could also be used to (for example) create a command line tool for optimising templates

davidmoshal commented 10 years ago

nice, what's the best branch to use to check it out? is it documented? David

Rich-Harris commented 10 years ago

It's supported in the most recent stable version - http://cdn.ractivejs.org/latest/ractive.js. Documentation is, err... coming... actually that's a good question. I'll knock something up. The short version is that a component looks like this:

<!-- optionally, import any sub-components needed by this component -->
<link rel='ractive' href='foo.html'>

<!-- follow that with a template -->
<h1>This is an imported component</h1>
<p>and this is a sub-component: <foo/></p>

<!-- optionally, include a style block (this will be encapsulated,
     so it doesn't leak into the page) -->
<style>
  p { color: red; }
</style>

<!-- optionally, include a script block defining behaviours etc -->
<script>
  // Dependencies are declared with `require()` - the exact meaning of this
  // depends on the loader being used. E.g. with the AMD loader it means
  // 'load this dependency, then initialise the component', with the Ractive.load()
  // plugin it means 'return `Ractive.lib.myLibrary` or `window.myLibrary`'
  var myLibrary = require( 'myLibrary' );

  // `component.exports` should basically be what you'd normally use
  // with `Ractive.extend(...)`, except that the template is already specified
  component.exports = {
    init: function () {
      alert( 'initing component' );
    },

    data: {
      letters: [ 'a', 'b', 'c' ]
    }
  };
</script>
Rich-Harris commented 10 years ago

This is long, long overdue, but I've finally got round to putting some docs together on how to author and use component files, in the form of an informal specification: https://github.com/ractivejs/component-spec.

davidmoshal commented 10 years ago

Thx, will take a look. On Aug 13, 2014 2:31 PM, "Rich Harris" notifications@github.com wrote:

This is long, long overdue, but I've finally got round to putting some docs together on how to author and use component files, in the form of an informal specification: https://github.com/ractivejs/component-spec.

— Reply to this email directly or view it on GitHub https://github.com/ractivejs/ractive/issues/366#issuecomment-52036942.

fskreuz commented 10 years ago

@Rich-Harris Awesome speech https://github.com/ractivejs/component-spec#single-file-components, especially the "cognitive burden" and "properly encapsulated UI components require you to consider those languages jointly, not separately". I completely agree. Makes for a good argument in team meetings and promote Ractive for widespread use in our project. :+1:

Rich-Harris commented 10 years ago

@fskreuz Thanks :-)

paulocoghi commented 9 years ago

@Rich-Harris I am returning to front-end development thanks to your framework. Probably this is not the best place but: Thank you very much for your incredible work.

I have spent almost 15 years programming in classic way of PHP/MySQL and with front-ends that were completely dependent to PHP processing.

I feel that, now, I can create a fully independent front-end applications with a completely fresh experience.

I hope that, someday, I will have enough knowledge to contribute to Ractive to make it even better, stronger and faster ;)

Rich-Harris commented 9 years ago

@paulocoghi Thanks so much for saying so! Really glad to hear it's working for you, and your contributions will be very welcome when the time comes. Not just my framework though, I'm one of many contributors :)

ceremcem commented 9 years ago

Hi,

Thank you for your hard work. RactiveUI idea is the most important one of my expectations from Ractive. It seems to be on its way.

But, I found it a little bit hard to use. It's hard to me, because I didn't manage to use it yet :)

First question on my mind (which draws me back while searching the way) is "Why I have to write more than a line of code when I cut my partials and paste into another file"? I think I should add only one more line, which means "buddy, look that file for your partials". If I need to point more than a file, then it's OK to point these files separately.

Second problem is, should I have to use JavaScript to code dynamic parts of the partial? For example, I prefer LiveScript. Likewise, styles could be written in any language that could be compiled to CSS, I think.

So I think there is something wrong with the mechanism. I'll (have to) stick with the deprecated "inline partial comments" for now. Do I miss something?