Really Simple Single-File-Component Support for Ember 3.x+

Hey everyone! I’m designing a new hobby project, and I really wanted to work with single file components, so I took an hour or so of my Sunday afternoon and put together this script:

https://gist.github.com/josiahbryan/fb9022d79ab909a7173c5d4855facadf

This is, as it says on the tin, really simple single-file-component support. What does it do?

  • Take one file (my-component.hbs) with <template>, <script>, and <style> tags (see sample below)…
  • And write out each tag into it’s own file where Ember expects to find it:
    • The <template> gets written to app/templates/components/my-component.hbs
    • The <style> gets written to app/styles/components/my-component.scss (NB .scss not .css - feel free to edit the script if you’re not using SASS)
    • The <script> gets written to app/components/my-component.js

When you combine this scripts built-in support for watching the files for changes and re-writing those files above, combined with ember serves built-in live reloading during development, then development on your Single-File-Component is just as simple as editing your single file app/sfcs/my-component.hbs and this script will write out those three files automatically, and ember will auto-detect and auto-rebuild your app bundle when it sees those files changed.

Feedback / suggestions all welcome!

To integrate into your Ember project, save the script from the gist above somewhere. (I created a folder called myproject/utils/ and put it there.) Then, add this line at the top of ember-cli-build.js: require('./utils/debundle-sfc').watchAndRebuild();

IMPORTANT: You will HAVE to change the config.appRoot value embedded in the script, because it’s set for my app, and I’m guessing you don’t have the exact same folder path as I do. Notes about appRoot:

  • appRoot must end with a slash (/)
  • appRoot must be the root of the Ember app (e.g. where the components/ folder is, etc.)

This script assumes you put your single-file-components in a folder under app/ called sfcs. Change config below to change where they’re stored. Note this script expects your component files to have .hbs as the extension.

Obviously, you’ll have to create the sfcs folder (app/sfcs) manually since Ember doesn’t generate it.

You can run this script manually from the command line, either one-time (node script.js) or with a --watch argument to start the file watcher and rebuild (node script.js --watch) (Note: We watch the app/sfcs folder, not the individual files, so you can create new files in that folder and the script will automatically process those as well.) Note, we don’t REMOVE the generated files if you remove the component from app/sfcs - that’s an idea for future improvement, of course.

Obviously, when you add it to ember-cli-build with the .watchAndRebuild() call as shown above, you don’t have to run it manually on the command line during development (assuming you have ember serve) running in a console somwhere as you work.

Note: I use Atom as my text editor, and it’s built-in syntax highlighting for HBS “just works”.

Example single-file-component, I put the contents in app/sfcs/hello-world.hbs:

    <template>
       <h1 local-class='header'>
           Hello, <b {{action 'randomName'}}>{{if name name 'World'}}!</b>
       </h1>
    </template>

    <style>
       .header {
           /* For example ... */
           background: #ccc;
       }
    </style>

    <script>
       // Script automatically appends Component import to generated .js file, other imports are up to you to add here
       export default Component.extend({
           tagName: '',
           actions: {
               randomName() {
                   this.set('name', "Person # " + (Math.random() * 1024).toFixed(0));
               }
           },
       });
    </script>
2 Likes

This looks interesting and really promising!

It seems like a next step might be to make this into an addon and have this work directly with ember-cli/Broccoli so you don’t need the separate watch. Have you tried anything along those lines? (I think this might be something that broccoli is really good at handling!)

You’re absolutely right, making it into an add-on and integrating it into the main build flow would be the far more elegant way of doing it.

Making the add-on isn’t that intimidating, but connecting to get into the main build flow does sound rather intimidating. I don’t have any experience digging around the build flow internals or writing plugins into Brocli. Any pointers on how to easily plug into it or an example page somewhere on how to do that?

Thanks for the feedback! Totally agree, it would be far more elegant to simply install this as an add-on

you could checkout the ember-cli-typescript project, since it does pretty much the same thing. Takes some files, transforms them, puts them in the correct location, and the build proceeds.

Hey thanks for the pointer to the typescript project - I did indeed checkout the source from GitHub a little while ago, and I tried to find the entry point for the plugin to the build process - it looks like it’s in lib/incremental-typescript-compiler/index.js, but that file is incredibly complex - and it’s actually taken me more time to try to read and figure out how to write a plugin than it took to write the the original script haha!

If someone can point me to a simple example of of a plugin that transforms files in the build process - sort of a proof-of-concept…? That I could then merge my script into that…?

Otherwise, for now, this script certainly does what I need it to do for my purposes - but I’ll be glad to polish and publish if it is desired!

A few resources to get started that may be helpful:

First — this blog post form Oli Griffiths is a must read: Ember Cli & Broccoli 2.0 | Oli Griffiths …it gives a great overview of the mental model required to work with Broccoli (it definitely takes some getting used to).

Then, my go-to for Broccoli inspiration is ember-css-modules. First, the index.js shows how the add-on itself is configured. Then, there’s a couple of examples of the Broccoli Plugins used.

I think the shell of solving this would be a combination of using the preprocessTree hook in the index.js of the addon and creating a broccoli plugin for splitting an input file into its corresponding js, hbs, and css files.

In the plugin itself, you may also look to using something like broccoli-file-creator for adding files to the app’s tree.

2 Likes

Yes, what @Spencer_Price said.

I think preprocessTree can do what you want. The easiest way to experiment and see what is being handed to it is to use broccoli-debug to dump the trees you’re given.

2 Likes