diff --git a/Brocfile.js b/Brocfile.js index 8deab3dba89..ac5f702a80d 100644 --- a/Brocfile.js +++ b/Brocfile.js @@ -86,7 +86,11 @@ function defeatureifyConfig(opts) { /* Returns a tree picked from `packages/#{packageName}/lib` and then move `main.js` to `/#{packageName}.js`. */ -function vendoredPackage(packageName) { +function vendoredPackage(packageName, options) { + if (!options) { options = {}; } + + var libPath = options.libPath || 'packages/' + packageName + '/lib'; + var mainFile = options.mainFile || 'main.js'; /* For example: Given the following dir: @@ -97,8 +101,8 @@ function vendoredPackage(packageName) { /metamorph └── main.js */ - var libTree = pickFiles('packages/' + packageName + '/lib', { - files: ['main.js'], + var libTree = pickFiles(libPath, { + files: [ mainFile ], srcDir: '/', destDir: '/' + packageName }); @@ -113,7 +117,7 @@ function vendoredPackage(packageName) { └── metamorph.js */ var sourceTree = moveFile(libTree, { - srcFile: packageName + '/main.js', + srcFile: packageName + '/' + mainFile, destFile: '/' + packageName + '.js' }); @@ -337,14 +341,23 @@ var iifeStop = writeFile('iife-stop', '})();'); 'ember-metal': {trees: null, vendorRequirements: ['backburner']} ``` */ +var handlebarsConfig = { + libPath: 'node_modules/handlebars/dist', + mainFile: 'handlebars.amd.js' +}; + var vendoredPackages = { - 'loader': vendoredPackage('loader'), - 'rsvp': vendoredEs6Package('rsvp'), - 'backburner': vendoredEs6Package('backburner'), - 'router': vendoredEs6Package('router.js'), - 'route-recognizer': vendoredEs6Package('route-recognizer'), - 'dag-map': vendoredEs6Package('dag-map'), - 'morph': htmlbarsPackage('morph') + 'loader': vendoredPackage('loader'), + 'rsvp': vendoredEs6Package('rsvp'), + 'backburner': vendoredEs6Package('backburner'), + 'router': vendoredEs6Package('router.js'), + 'route-recognizer': vendoredEs6Package('route-recognizer'), + 'dag-map': vendoredEs6Package('dag-map'), + 'morph': htmlbarsPackage('morph'), + 'htmlbars': htmlbarsPackage('htmlbars-runtime'), + 'htmlbars-compiler': htmlbarsPackage('htmlbars-compiler'), + 'simple-html-tokenizer': vendoredEs6Package('simple-html-tokenizer'), + 'handlebars': vendoredPackage('handlebars', handlebarsConfig) }; var emberHandlebarsCompiler = pickFiles('packages/ember-handlebars-compiler/lib', { diff --git a/lib/packages.js b/lib/packages.js index 136d072a1c8..fe03d68cb2d 100644 --- a/lib/packages.js +++ b/lib/packages.js @@ -9,6 +9,7 @@ module.exports = { 'ember-testing': {trees: null, requirements: ['ember-application', 'ember-routing'], developmentOnly: true}, 'ember-handlebars-compiler': {trees: null, requirements: ['ember-views']}, 'ember-handlebars': {trees: null, requirements: ['ember-views', 'ember-handlebars-compiler']}, + 'ember-htmlbars': {trees: null, vendorRequirements: ['htmlbars', 'handlebars', 'simple-html-tokenizer', 'htmlbars-compiler'], requirements: ['ember-metal-views']}, 'ember-routing': {trees: null, vendorRequirements: ['router', 'route-recognizer'], requirements: ['ember-runtime', 'ember-views']}, 'ember-routing-handlebars': {trees: null, requirements: ['ember-routing', 'ember-handlebars']}, diff --git a/packages/ember-htmlbars/lib/hooks.js b/packages/ember-htmlbars/lib/hooks.js new file mode 100644 index 00000000000..1ae1a96e74b --- /dev/null +++ b/packages/ember-htmlbars/lib/hooks.js @@ -0,0 +1,112 @@ +import Stream from "ember-metal/streams/stream"; +import {readArray} from "ember-metal/streams/read"; + +function streamifyArgs(context, params, options, env) { + var hooks = env.hooks; + + // TODO: Revisit keyword rewriting approach + if (params.length === 3 && params[1] === "in") { + params.splice(0, 3, {isKeyword: true, from: params[2], to: params[0]}); + options.types.splice(0, 3, 'keyword'); + } + + if (params.length === 3 && params[1] === "as") { + params.splice(0, 3, {isKeyword: true, from: params[0], to: params[2]}); + options.types.splice(0, 3, 'keyword'); + } + + // Convert ID params to streams + for (var i = 0, l = params.length; i < l; i++) { + if (options.types[i] === 'id') { + params[i] = hooks.streamFor(context, params[i]); + } else if (options.types[i] === 'keyword') { + params[i].lazyValue = hooks.streamFor(context, params[i].from); + } + } + + // Convert hash ID values to streams + var hash = options.hash, + hashTypes = options.hashTypes; + for (var key in hash) { + if (hashTypes[key] === 'id') { + hash[key] = hooks.streamFor(context, hash[key]); + } + } +} + +export function content(morph, path, context, params, options, env) { + var hooks = env.hooks; + + // TODO: just set escaped on the morph in HTMLBars + morph.escaped = options.escaped; + var lazyValue; + var helper = hooks.lookupHelper(path, env); + if (helper) { + streamifyArgs(context, params, options, env); + lazyValue = helper(params, options, env); + } else { + lazyValue = hooks.streamFor(context, path); + } + if (lazyValue) { + lazyValue.subscribe(function(sender) { + morph.update(sender.value()); + }); + + morph.update(lazyValue.value()); + } +} + +export function element(element, path, context, params, options, env) { //jshint ignore:line + var hooks = env.hooks; + var helper = hooks.lookupHelper(path, env); + + if (helper) { + streamifyArgs(context, params, options, env); + return helper(element, params, options, env); + } else { + return hooks.streamFor(context, path); + } +} + +export function subexpr(path, context, params, options, env) { + var hooks = env.hooks; + var helper = hooks.lookupHelper(path, env); + + if (helper) { + streamifyArgs(context, params, options, env); + return helper(params, options, env); + } else { + return hooks.streamFor(context, path); + } +} + +export function lookupHelper(name, env) { + if (name === 'concat') { return concat; } + if (name === 'attribute') { return attribute; } + return env.helpers[name]; +} + +function attribute(element, params, options) { + var name = params[0], + value = params[1]; + + value.subscribe(function(lazyValue) { + element.setAttribute(name, lazyValue.value()); + }); + + element.setAttribute(name, value.value()); +} + +function concat(params, options) { + var stream = new Stream(function() { + return readArray(params).join(''); + }); + + params.forEach(function(param) { + if (param && param.isStream) { + param.subscribe(stream.notifyAll, stream); + } + }); + + return stream; +} diff --git a/packages/ember-htmlbars/lib/main.js b/packages/ember-htmlbars/lib/main.js new file mode 100644 index 00000000000..fbdc0ed74c2 --- /dev/null +++ b/packages/ember-htmlbars/lib/main.js @@ -0,0 +1,24 @@ +import { content, element, subexpr, lookupHelper } from "ember-htmlbars/hooks"; +import { DOMHelper } from "morph"; +import Stream from "ember-metal/streams/stream"; + +export var defaultEnv = { + dom: new DOMHelper(), + + hooks: { + content: content, + element: element, + subexpr: subexpr, + lookupHelper: lookupHelper, + + streamFor: function(context, path) { + return new Stream(function() { + return context[path]; + }); + } + }, + + helpers: { + + } +}; diff --git a/packages/ember-htmlbars/tests/htmlbars_test.js b/packages/ember-htmlbars/tests/htmlbars_test.js new file mode 100644 index 00000000000..1669580b09b --- /dev/null +++ b/packages/ember-htmlbars/tests/htmlbars_test.js @@ -0,0 +1,23 @@ +import { compile } from "htmlbars-compiler/compiler"; +import { defaultEnv } from "ember-htmlbars"; + +function fragmentHTML(fragment) { + var html = '', node; + for (var i = 0, l = fragment.childNodes.length; i < l; i++) { + node = fragment.childNodes[i]; + if (node.nodeType === 3) { + html += node.nodeValue; + } else { + html += node.outerHTML; + } + } + return html; +} + +QUnit.module("ember-htmlbars"); + +test("hello world", function() { + var template = compile("ohai {{name}}"); + var output = template({name: 'erik'}, defaultEnv, document.body); + equal(fragmentHTML(output), "ohai erik"); +});