mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
basic project infrastructure
This commit is contained in:
@@ -3,3 +3,16 @@
|
|||||||
This is a POC, not at all production ready code!!!
|
This is a POC, not at all production ready code!!!
|
||||||
|
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
npm run test
|
||||||
|
npm run test:watch
|
||||||
|
|
||||||
|
## Build code bundles
|
||||||
|
|
||||||
|
npm run bundle:core
|
||||||
|
npm run bundle:core:watch
|
||||||
|
|
||||||
|
## Demo page
|
||||||
|
|
||||||
|
npm run demo
|
||||||
+434
@@ -0,0 +1,434 @@
|
|||||||
|
/**
|
||||||
|
* @license almond 0.3.3 Copyright jQuery Foundation and other contributors.
|
||||||
|
* Released under MIT license, http://github.com/requirejs/almond/LICENSE
|
||||||
|
*/
|
||||||
|
//Going sloppy to avoid 'use strict' string cost, but strict practices should
|
||||||
|
//be followed.
|
||||||
|
/*global setTimeout: false */
|
||||||
|
|
||||||
|
var requirejs, require, define;
|
||||||
|
(function (undef) {
|
||||||
|
var main, req, makeMap, handlers,
|
||||||
|
defined = {},
|
||||||
|
waiting = {},
|
||||||
|
config = {},
|
||||||
|
defining = {},
|
||||||
|
hasOwn = Object.prototype.hasOwnProperty,
|
||||||
|
aps = [].slice,
|
||||||
|
jsSuffixRegExp = /\.js$/;
|
||||||
|
|
||||||
|
function hasProp(obj, prop) {
|
||||||
|
return hasOwn.call(obj, prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a relative module name, like ./something, normalize it to
|
||||||
|
* a real name that can be mapped to a path.
|
||||||
|
* @param {String} name the relative name
|
||||||
|
* @param {String} baseName a real name that the name arg is relative
|
||||||
|
* to.
|
||||||
|
* @returns {String} normalized name
|
||||||
|
*/
|
||||||
|
function normalize(name, baseName) {
|
||||||
|
var nameParts, nameSegment, mapValue, foundMap, lastIndex,
|
||||||
|
foundI, foundStarMap, starI, i, j, part, normalizedBaseParts,
|
||||||
|
baseParts = baseName && baseName.split("/"),
|
||||||
|
map = config.map,
|
||||||
|
starMap = (map && map['*']) || {};
|
||||||
|
|
||||||
|
//Adjust any relative paths.
|
||||||
|
if (name) {
|
||||||
|
name = name.split('/');
|
||||||
|
lastIndex = name.length - 1;
|
||||||
|
|
||||||
|
// If wanting node ID compatibility, strip .js from end
|
||||||
|
// of IDs. Have to do this here, and not in nameToUrl
|
||||||
|
// because node allows either .js or non .js to map
|
||||||
|
// to same file.
|
||||||
|
if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) {
|
||||||
|
name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts with a '.' so need the baseName
|
||||||
|
if (name[0].charAt(0) === '.' && baseParts) {
|
||||||
|
//Convert baseName to array, and lop off the last part,
|
||||||
|
//so that . matches that 'directory' and not name of the baseName's
|
||||||
|
//module. For instance, baseName of 'one/two/three', maps to
|
||||||
|
//'one/two/three.js', but we want the directory, 'one/two' for
|
||||||
|
//this normalization.
|
||||||
|
normalizedBaseParts = baseParts.slice(0, baseParts.length - 1);
|
||||||
|
name = normalizedBaseParts.concat(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
//start trimDots
|
||||||
|
for (i = 0; i < name.length; i++) {
|
||||||
|
part = name[i];
|
||||||
|
if (part === '.') {
|
||||||
|
name.splice(i, 1);
|
||||||
|
i -= 1;
|
||||||
|
} else if (part === '..') {
|
||||||
|
// If at the start, or previous value is still ..,
|
||||||
|
// keep them so that when converted to a path it may
|
||||||
|
// still work when converted to a path, even though
|
||||||
|
// as an ID it is less than ideal. In larger point
|
||||||
|
// releases, may be better to just kick out an error.
|
||||||
|
if (i === 0 || (i === 1 && name[2] === '..') || name[i - 1] === '..') {
|
||||||
|
continue;
|
||||||
|
} else if (i > 0) {
|
||||||
|
name.splice(i - 1, 2);
|
||||||
|
i -= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//end trimDots
|
||||||
|
|
||||||
|
name = name.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
//Apply map config if available.
|
||||||
|
if ((baseParts || starMap) && map) {
|
||||||
|
nameParts = name.split('/');
|
||||||
|
|
||||||
|
for (i = nameParts.length; i > 0; i -= 1) {
|
||||||
|
nameSegment = nameParts.slice(0, i).join("/");
|
||||||
|
|
||||||
|
if (baseParts) {
|
||||||
|
//Find the longest baseName segment match in the config.
|
||||||
|
//So, do joins on the biggest to smallest lengths of baseParts.
|
||||||
|
for (j = baseParts.length; j > 0; j -= 1) {
|
||||||
|
mapValue = map[baseParts.slice(0, j).join('/')];
|
||||||
|
|
||||||
|
//baseName segment has config, find if it has one for
|
||||||
|
//this name.
|
||||||
|
if (mapValue) {
|
||||||
|
mapValue = mapValue[nameSegment];
|
||||||
|
if (mapValue) {
|
||||||
|
//Match, update name to the new value.
|
||||||
|
foundMap = mapValue;
|
||||||
|
foundI = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundMap) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check for a star map match, but just hold on to it,
|
||||||
|
//if there is a shorter segment match later in a matching
|
||||||
|
//config, then favor over this star map.
|
||||||
|
if (!foundStarMap && starMap && starMap[nameSegment]) {
|
||||||
|
foundStarMap = starMap[nameSegment];
|
||||||
|
starI = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundMap && foundStarMap) {
|
||||||
|
foundMap = foundStarMap;
|
||||||
|
foundI = starI;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundMap) {
|
||||||
|
nameParts.splice(0, foundI, foundMap);
|
||||||
|
name = nameParts.join('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRequire(relName, forceSync) {
|
||||||
|
return function () {
|
||||||
|
//A version of a require function that passes a moduleName
|
||||||
|
//value for items that may need to
|
||||||
|
//look up paths relative to the moduleName
|
||||||
|
var args = aps.call(arguments, 0);
|
||||||
|
|
||||||
|
//If first arg is not require('string'), and there is only
|
||||||
|
//one arg, it is the array form without a callback. Insert
|
||||||
|
//a null so that the following concat is correct.
|
||||||
|
if (typeof args[0] !== 'string' && args.length === 1) {
|
||||||
|
args.push(null);
|
||||||
|
}
|
||||||
|
return req.apply(undef, args.concat([relName, forceSync]));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeNormalize(relName) {
|
||||||
|
return function (name) {
|
||||||
|
return normalize(name, relName);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeLoad(depName) {
|
||||||
|
return function (value) {
|
||||||
|
defined[depName] = value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function callDep(name) {
|
||||||
|
if (hasProp(waiting, name)) {
|
||||||
|
var args = waiting[name];
|
||||||
|
delete waiting[name];
|
||||||
|
defining[name] = true;
|
||||||
|
main.apply(undef, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasProp(defined, name) && !hasProp(defining, name)) {
|
||||||
|
throw new Error('No ' + name);
|
||||||
|
}
|
||||||
|
return defined[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
//Turns a plugin!resource to [plugin, resource]
|
||||||
|
//with the plugin being undefined if the name
|
||||||
|
//did not have a plugin prefix.
|
||||||
|
function splitPrefix(name) {
|
||||||
|
var prefix,
|
||||||
|
index = name ? name.indexOf('!') : -1;
|
||||||
|
if (index > -1) {
|
||||||
|
prefix = name.substring(0, index);
|
||||||
|
name = name.substring(index + 1, name.length);
|
||||||
|
}
|
||||||
|
return [prefix, name];
|
||||||
|
}
|
||||||
|
|
||||||
|
//Creates a parts array for a relName where first part is plugin ID,
|
||||||
|
//second part is resource ID. Assumes relName has already been normalized.
|
||||||
|
function makeRelParts(relName) {
|
||||||
|
return relName ? splitPrefix(relName) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a name map, normalizing the name, and using a plugin
|
||||||
|
* for normalization if necessary. Grabs a ref to plugin
|
||||||
|
* too, as an optimization.
|
||||||
|
*/
|
||||||
|
makeMap = function (name, relParts) {
|
||||||
|
var plugin,
|
||||||
|
parts = splitPrefix(name),
|
||||||
|
prefix = parts[0],
|
||||||
|
relResourceName = relParts[1];
|
||||||
|
|
||||||
|
name = parts[1];
|
||||||
|
|
||||||
|
if (prefix) {
|
||||||
|
prefix = normalize(prefix, relResourceName);
|
||||||
|
plugin = callDep(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Normalize according
|
||||||
|
if (prefix) {
|
||||||
|
if (plugin && plugin.normalize) {
|
||||||
|
name = plugin.normalize(name, makeNormalize(relResourceName));
|
||||||
|
} else {
|
||||||
|
name = normalize(name, relResourceName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
name = normalize(name, relResourceName);
|
||||||
|
parts = splitPrefix(name);
|
||||||
|
prefix = parts[0];
|
||||||
|
name = parts[1];
|
||||||
|
if (prefix) {
|
||||||
|
plugin = callDep(prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Using ridiculous property names for space reasons
|
||||||
|
return {
|
||||||
|
f: prefix ? prefix + '!' + name : name, //fullName
|
||||||
|
n: name,
|
||||||
|
pr: prefix,
|
||||||
|
p: plugin
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeConfig(name) {
|
||||||
|
return function () {
|
||||||
|
return (config && config.config && config.config[name]) || {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers = {
|
||||||
|
require: function (name) {
|
||||||
|
return makeRequire(name);
|
||||||
|
},
|
||||||
|
exports: function (name) {
|
||||||
|
var e = defined[name];
|
||||||
|
if (typeof e !== 'undefined') {
|
||||||
|
return e;
|
||||||
|
} else {
|
||||||
|
return (defined[name] = {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
module: function (name) {
|
||||||
|
return {
|
||||||
|
id: name,
|
||||||
|
uri: '',
|
||||||
|
exports: defined[name],
|
||||||
|
config: makeConfig(name)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
main = function (name, deps, callback, relName) {
|
||||||
|
var cjsModule, depName, ret, map, i, relParts,
|
||||||
|
args = [],
|
||||||
|
callbackType = typeof callback,
|
||||||
|
usingExports;
|
||||||
|
|
||||||
|
//Use name if no relName
|
||||||
|
relName = relName || name;
|
||||||
|
relParts = makeRelParts(relName);
|
||||||
|
|
||||||
|
//Call the callback to define the module, if necessary.
|
||||||
|
if (callbackType === 'undefined' || callbackType === 'function') {
|
||||||
|
//Pull out the defined dependencies and pass the ordered
|
||||||
|
//values to the callback.
|
||||||
|
//Default to [require, exports, module] if no deps
|
||||||
|
deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps;
|
||||||
|
for (i = 0; i < deps.length; i += 1) {
|
||||||
|
map = makeMap(deps[i], relParts);
|
||||||
|
depName = map.f;
|
||||||
|
|
||||||
|
//Fast path CommonJS standard dependencies.
|
||||||
|
if (depName === "require") {
|
||||||
|
args[i] = handlers.require(name);
|
||||||
|
} else if (depName === "exports") {
|
||||||
|
//CommonJS module spec 1.1
|
||||||
|
args[i] = handlers.exports(name);
|
||||||
|
usingExports = true;
|
||||||
|
} else if (depName === "module") {
|
||||||
|
//CommonJS module spec 1.1
|
||||||
|
cjsModule = args[i] = handlers.module(name);
|
||||||
|
} else if (hasProp(defined, depName) ||
|
||||||
|
hasProp(waiting, depName) ||
|
||||||
|
hasProp(defining, depName)) {
|
||||||
|
args[i] = callDep(depName);
|
||||||
|
} else if (map.p) {
|
||||||
|
map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {});
|
||||||
|
args[i] = defined[depName];
|
||||||
|
} else {
|
||||||
|
throw new Error(name + ' missing ' + depName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = callback ? callback.apply(defined[name], args) : undefined;
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
//If setting exports via "module" is in play,
|
||||||
|
//favor that over return value and exports. After that,
|
||||||
|
//favor a non-undefined return value over exports use.
|
||||||
|
if (cjsModule && cjsModule.exports !== undef &&
|
||||||
|
cjsModule.exports !== defined[name]) {
|
||||||
|
defined[name] = cjsModule.exports;
|
||||||
|
} else if (ret !== undef || !usingExports) {
|
||||||
|
//Use the return value from the function.
|
||||||
|
defined[name] = ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (name) {
|
||||||
|
//May just be an object definition for the module. Only
|
||||||
|
//worry about defining if have a module name.
|
||||||
|
defined[name] = callback;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
requirejs = require = req = function (deps, callback, relName, forceSync, alt) {
|
||||||
|
if (typeof deps === "string") {
|
||||||
|
if (handlers[deps]) {
|
||||||
|
//callback in this case is really relName
|
||||||
|
return handlers[deps](callback);
|
||||||
|
}
|
||||||
|
//Just return the module wanted. In this scenario, the
|
||||||
|
//deps arg is the module name, and second arg (if passed)
|
||||||
|
//is just the relName.
|
||||||
|
//Normalize module name, if it contains . or ..
|
||||||
|
return callDep(makeMap(deps, makeRelParts(callback)).f);
|
||||||
|
} else if (!deps.splice) {
|
||||||
|
//deps is a config object, not an array.
|
||||||
|
config = deps;
|
||||||
|
if (config.deps) {
|
||||||
|
req(config.deps, config.callback);
|
||||||
|
}
|
||||||
|
if (!callback) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback.splice) {
|
||||||
|
//callback is an array, which means it is a dependency list.
|
||||||
|
//Adjust args if there are dependencies
|
||||||
|
deps = callback;
|
||||||
|
callback = relName;
|
||||||
|
relName = null;
|
||||||
|
} else {
|
||||||
|
deps = undef;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Support require(['a'])
|
||||||
|
callback = callback || function () {};
|
||||||
|
|
||||||
|
//If relName is a function, it is an errback handler,
|
||||||
|
//so remove it.
|
||||||
|
if (typeof relName === 'function') {
|
||||||
|
relName = forceSync;
|
||||||
|
forceSync = alt;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Simulate async callback;
|
||||||
|
if (forceSync) {
|
||||||
|
main(undef, deps, callback, relName);
|
||||||
|
} else {
|
||||||
|
//Using a non-zero value because of concern for what old browsers
|
||||||
|
//do, and latest browsers "upgrade" to 4 if lower value is used:
|
||||||
|
//http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout:
|
||||||
|
//If want a value immediately, use require('id') instead -- something
|
||||||
|
//that works in almond on the global level, but not guaranteed and
|
||||||
|
//unlikely to work in other AMD implementations.
|
||||||
|
setTimeout(function () {
|
||||||
|
main(undef, deps, callback, relName);
|
||||||
|
}, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return req;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Just drops the config on the floor, but returns req in case
|
||||||
|
* the config return value is used.
|
||||||
|
*/
|
||||||
|
req.config = function (cfg) {
|
||||||
|
return req(cfg);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expose module registry for debugging and tooling
|
||||||
|
*/
|
||||||
|
requirejs._defined = defined;
|
||||||
|
|
||||||
|
define = function (name, deps, callback) {
|
||||||
|
if (typeof name !== 'string') {
|
||||||
|
throw new Error('See almond README: incorrect module build, no module name');
|
||||||
|
}
|
||||||
|
|
||||||
|
//This module may not have dependencies
|
||||||
|
if (!deps.splice) {
|
||||||
|
//deps is not an array, so probably means
|
||||||
|
//an object literal or factory function for
|
||||||
|
//the value. Adjust args.
|
||||||
|
callback = deps;
|
||||||
|
deps = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasProp(defined, name) && !hasProp(waiting, name)) {
|
||||||
|
waiting[name] = [name, deps, callback];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
define.amd = {
|
||||||
|
jQuery: true
|
||||||
|
};
|
||||||
|
}());
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Odoo Web Core Demo</title>
|
||||||
|
<script src="almond.js"></script>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
<script>
|
||||||
|
require('main');
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import Widget from "../../src/core/widget";
|
||||||
|
|
||||||
|
const template = `
|
||||||
|
<div t-debug="1">
|
||||||
|
<button t-on-click="increment(-1)">-</button>
|
||||||
|
<span>Value: <t t-esc="state.counter"/></span>
|
||||||
|
<button t-on-click="increment(1)">+</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default class Counter extends Widget {
|
||||||
|
name = "counter";
|
||||||
|
template = template;
|
||||||
|
state = {
|
||||||
|
counter: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(parent: Widget, initialState?: number) {
|
||||||
|
super(parent);
|
||||||
|
this.state.counter = initialState || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
increment(delta: number) {
|
||||||
|
this.updateState({ counter: this.state.counter + delta });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Env } from "../../src/core/widget";
|
||||||
|
import QWeb from "../../src/core/qweb";
|
||||||
|
|
||||||
|
const qweb = new QWeb();
|
||||||
|
|
||||||
|
const env: Env = {
|
||||||
|
qweb: qweb,
|
||||||
|
services: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default env;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
///<amd-module name="main" />
|
||||||
|
|
||||||
|
// import RootWidget from "./RootWidget";
|
||||||
|
import Counter from "./Counter";
|
||||||
|
import env from "./env";
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", async function() {
|
||||||
|
const rootWidget = new Counter(null);
|
||||||
|
rootWidget.setEnvironment(env);
|
||||||
|
const mainDiv = document.getElementById("app")!;
|
||||||
|
await rootWidget.mount(mainDiv);
|
||||||
|
});
|
||||||
+9
-3
@@ -4,10 +4,12 @@
|
|||||||
"description": "Core Utils for Odoo Web Client",
|
"description": "Core Utils for Odoo Web Client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -m amd --lib es2017,dom --target esnext --outfile dist/bundle.js",
|
|
||||||
"build:watch": "tsc --watch -m amd --lib es2017,dom --target esnext --outfile dist/bundle.js",
|
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch"
|
"test:watch": "jest --watch",
|
||||||
|
"bundle:core": "tsc -m es6 --lib es2017,dom --target esnext --outdir dist src/bundles/core.ts && rollup dist/bundles/core.js --file dist/bundle-core.js --format iife",
|
||||||
|
"bundle:core:watch": "nodemon -w src -e ts -x 'npm run bundle:core'",
|
||||||
|
"demo": "cp -r demo/ dist/demo && tsc -m amd --lib es2017,dom --target esnext --outfile dist/demo/main.js demo/src/main.ts",
|
||||||
|
"postdemo": "open dist/demo/index.html"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -22,6 +24,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^23.3.12",
|
"@types/jest": "^23.3.12",
|
||||||
"jest": "^23.6.0",
|
"jest": "^23.6.0",
|
||||||
|
"nodemon": "^1.18.9",
|
||||||
|
"rollup": "^1.1.0",
|
||||||
|
"rollup-plugin-typescript2": "^0.19.0",
|
||||||
|
"source-map-support": "^0.5.10",
|
||||||
"ts-jest": "^23.10.5",
|
"ts-jest": "^23.10.5",
|
||||||
"typescript": "^3.2.2"
|
"typescript": "^3.2.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import QWeb from "../core/qweb";
|
||||||
|
import * as utils from "../core/utils";
|
||||||
|
import Widget from "../core/widget";
|
||||||
|
|
||||||
|
|
||||||
|
(<any>window).core = {
|
||||||
|
QWeb,
|
||||||
|
utils,
|
||||||
|
Widget
|
||||||
|
}
|
||||||
@@ -75,9 +75,9 @@ export class Context {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
addLine(line: string) {
|
addLine(line: string) {
|
||||||
const prefix = new Array(this.indentLevel).join('\t');
|
const prefix = new Array(this.indentLevel).join("\t");
|
||||||
const lastChar = line[line.length - 1];
|
const lastChar = line[line.length - 1];
|
||||||
const suffix = lastChar !=='}' && lastChar !== '{' ? ';' : '';
|
const suffix = lastChar !== "}" && lastChar !== "{" ? ";" : "";
|
||||||
this.code.push(prefix + line + suffix);
|
this.code.push(prefix + line + suffix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import QWeb from "./qweb";
|
||||||
|
|
||||||
|
export interface Env {
|
||||||
|
qweb: QWeb;
|
||||||
|
services: { [key: string]: any };
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Widget {
|
||||||
|
name: string = "widget";
|
||||||
|
template: string = "<div></div>";
|
||||||
|
|
||||||
|
parent: Widget | null;
|
||||||
|
children: Widget[] = [];
|
||||||
|
env: Env | null = null;
|
||||||
|
el: ChildNode | null = null;
|
||||||
|
state: Object = {};
|
||||||
|
refs: { [key: string]: Widget } = {};
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Lifecycle
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
constructor(parent: Widget | null, props?: any) {
|
||||||
|
this.parent = parent;
|
||||||
|
if (parent) {
|
||||||
|
parent.children.push(this);
|
||||||
|
this.setEnvironment(parent.env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async willStart() {}
|
||||||
|
|
||||||
|
mounted() {}
|
||||||
|
|
||||||
|
willUnmount() {}
|
||||||
|
|
||||||
|
destroyed() {}
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Public
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async mount(target: HTMLElement) {
|
||||||
|
await this.willStart();
|
||||||
|
await this.render();
|
||||||
|
target.appendChild(this.el!);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {}
|
||||||
|
|
||||||
|
setEnvironment(env: Env | null) {
|
||||||
|
this.env = env ? Object.create(env) : null;
|
||||||
|
if (this.env) {
|
||||||
|
this.env.qweb.addTemplate(this.name, this.template);
|
||||||
|
delete this.template;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateState(newState: Object) {
|
||||||
|
Object.assign(this.state, newState);
|
||||||
|
await this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Private
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async render() {
|
||||||
|
const fragment = await this.env!.qweb.render(this.name, this);
|
||||||
|
this._setElement(fragment.firstChild!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setElement(el: ChildNode) {
|
||||||
|
if (this.el) {
|
||||||
|
this.el.replaceWith(el);
|
||||||
|
}
|
||||||
|
this.el = el;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import QWeb from "../src/qweb";
|
import QWeb from "../src/core/qweb";
|
||||||
import { EvalContext } from "../src/qweb";
|
import { EvalContext } from "../src/core/qweb";
|
||||||
|
|
||||||
|
|
||||||
function renderToDOM(t: string, context: EvalContext = {}): DocumentFragment {
|
function renderToDOM(t: string, context: EvalContext = {}): DocumentFragment {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { escape, htmlTrim } from "../src/utils";
|
import { escape, htmlTrim } from "../src/core/utils";
|
||||||
|
|
||||||
describe("escape", () => {
|
describe("escape", () => {
|
||||||
test("normal strings", () => {
|
test("normal strings", () => {
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import Widget from "../src/core/widget";
|
||||||
|
import QWeb from "../src/core/qweb";
|
||||||
|
|
||||||
|
function makeWidget(W: typeof Widget): Widget {
|
||||||
|
const env = {
|
||||||
|
qweb: new QWeb(),
|
||||||
|
services: {}
|
||||||
|
};
|
||||||
|
const w = new W(null);
|
||||||
|
w.setEnvironment(env);
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function click(el: HTMLElement) {
|
||||||
|
el.click();
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = `
|
||||||
|
<div><t t-esc="state.counter"/><button t-on-click="inc">Inc</button></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default class Counter extends Widget {
|
||||||
|
name = "counter";
|
||||||
|
template = template;
|
||||||
|
state = {
|
||||||
|
counter: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
inc() {
|
||||||
|
this.updateState({ counter: this.state.counter + 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("basic widget properties", () => {
|
||||||
|
test("has no el after creation", async () => {
|
||||||
|
const widget = makeWidget(Widget);
|
||||||
|
expect(widget.el).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can be mounted", async () => {
|
||||||
|
const widget = makeWidget(Widget);
|
||||||
|
const target = document.createElement("div");
|
||||||
|
await widget.mount(target);
|
||||||
|
expect(target.innerHTML).toBe("<div></div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can be clicked on and updated", async () => {
|
||||||
|
const counter = makeWidget(Counter);
|
||||||
|
const target = document.createElement("div");
|
||||||
|
await counter.mount(target);
|
||||||
|
expect(target.innerHTML).toBe("<div>0<button>Inc</button></div>");
|
||||||
|
await click((<HTMLElement>counter.el).getElementsByTagName("button")[0]);
|
||||||
|
expect(target.innerHTML).toBe("<div>1<button>Inc</button></div>");
|
||||||
|
});
|
||||||
|
});
|
||||||
+1
-1
@@ -17,5 +17,5 @@
|
|||||||
"strictPropertyInitialization": true,
|
"strictPropertyInitialization": true,
|
||||||
"strictNullChecks": true
|
"strictNullChecks": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "tests/**/*.ts", "examples/**/*.ts"]
|
"include": ["src/**/*.ts", "tests/**/*.ts", "demo/**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user