Compare commits

...

12 Commits

Author SHA1 Message Date
Géry Debongnie ba20267151 [REL] v2.1.3
# v2.1.3

 - [FIX] components: properly differentiate t-call subcomponents
 - [REF] devtools: Better messages forwarding
 - [FIX] devtools: Fix app methods patching
 - [DOC] Fix a code bug in the example of slots
2023-04-29 09:46:05 +02:00
Géry Debongnie aabb7559b8 [FIX] components: properly differentiate t-call subcomponents
Before this commit, when using a dynamic t-call, it was possible to have
a collision between subcomponent keys. This would cause hard to
understand owl crash, because owl would incorrectly use the same
component instance for two different template location. From
owl point of view, one of these component has to be destroyed and the
other one should be mounted, so a crash would occur.

The fix is to simply properly key sub components.
2023-04-28 14:31:34 +02:00
Julien Carion (juca) 606d14a399 [REF] devtools: Better messages forwarding
This commit improves how messages are relayed from the page to the
devtools by using a specific port for each instance of the owl devtools
panel and refreshing it through the heartbeat message. This allows
to prevent sending messages to every devtools instances and sorting
the messages inside.
2023-04-25 09:31:53 +02:00
Julien Carion (juca) c049022798 [FIX] devtools: Fix app methods patching
This commit fixes how the apps methods are patched by decoupling
the patching of the destroy method from the rest because we need
to have a valid reference to a component node to do so (the root
node in this case). The previous behavior was to only patch methods
when an app with a root node is added but this was not working
when the app has no root node initially and gets one dynamically.
There is now a listener to patch the destroy method as soon as a
root node is set on any of the apps if it is not already the case.
2023-04-25 09:28:57 +02:00
myearn4 cae32c5bd4 [DOC] Fix a code bug in the example of slots
The value of activeTab should be tab_index rather than tab.
2023-04-25 09:24:31 +02:00
Géry Debongnie 63128b7d29 [REL] v2.1.2
# v2.1.2

 - [FIX] t-out: does not crash when outputting null
 - [FIX] playground: fix name of playground component
 - [FIX] playground: use ES owl version instead of iife
 - [IMP] playground: give a name so it is more explicit in devtools
 - [REF] github page: move page to docs folder on master branch
 - [IMP/FIX] playground: fix and improve various things
2023-04-24 16:29:43 +02:00
Géry Debongnie f9d810a42f [FIX] t-out: does not crash when outputting null
Before this commit, doing `t-out="null"` would crash, since the
safeOutput function would assume that it is a block, thanks to the fact
that `typeof null === 'object'`.

We handle here the null value just as if it was undefined: it outputs an
empty string.
2023-04-24 16:23:27 +02:00
Samuel Degueldre 1784a98b2c [FIX] playground: fix name of playground component 2023-04-24 15:53:09 +02:00
Géry Debongnie 52be5ae85f [FIX] playground: use ES owl version instead of iife
Otherwise, it will try to import owl from the iife build, which will not
work
2023-04-24 15:44:41 +02:00
Géry Debongnie 0765b19367 [IMP] playground: give a name so it is more explicit in devtools 2023-04-24 15:44:41 +02:00
Samuel Degueldre b8b5190bc6 [REF] github page: move page to docs folder on master branch
Currently, some of the things on the playground must be changed directly
on the master branch while other things require checking out the
gh-pages branch and making the modifications there. Even when changes
need to be made on the master branch, all changes that are not direcly
in owl itself need to be copied by hand to the gh-pages branch.

This commit moves all files that exist on the gh-pages branch to the
master branch in the docs folder, this change will require configuring
the github page to use the docs folder instead of a separate branch, but
will make maintenance of the github page and playground easier going
forward.
2023-04-24 15:32:51 +02:00
Samuel Degueldre 9475de4d18 [IMP/FIX] playground: fix and improve various things
The playground hasn't been touched for a while and a few things were
broken before this commit:
- Exporting as a standalone web application was broken because it still
attempted to load the templates like it was done in owl 1
- Resizing the editor tabs still tried to use `this.trigger`

While fixing the export feature, the files needed by the app were
factored out of hardcoded strings and into real files, as this makes it
easier to maintain, since these files get syntax highlighting.

The samples received the same treatment: there is now a `samples` folder
containing all the samples, it also contains a jsconfig, giving
autocompletion on owl functions while editing the files, and allowing
the owl import to look how it would when using owl as a node_module.
In the browser, this import is translated to the relative path of the
owl module using an import map.
2023-04-24 15:32:51 +02:00
75 changed files with 9431 additions and 2301 deletions
-3
View File
@@ -20,9 +20,6 @@ yarn-error.log*
node_modules
# Extras temp file
/tools/owl.js
release-notes.md
.rpt2_cache
+1 -1
View File
@@ -175,7 +175,7 @@ class Notebook extends Component {
<div class="notebook">
<div class="tabs">
<t t-foreach="tabNames" t-as="tab" t-key="tab_index">
<span t-att-class="{active:tab_index === activeTab}" t-on-click="() => state.activeTab=tab">
<span t-att-class="{active:tab_index === activeTab}" t-on-click="() => state.activeTab=tab_index">
<t t-esc="props.slots[tab].title"/>
</span>
</t>
File diff suppressed because one or more lines are too long
+72
View File
@@ -0,0 +1,72 @@
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
/* Tomorrow Comment */
.hljs-comment,
.hljs-quote {
color: #8e908c;
}
/* Tomorrow Red */
.hljs-variable,
.hljs-template-variable,
.hljs-tag,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class,
.hljs-regexp,
.hljs-deletion {
color: #c82829;
}
/* Tomorrow Orange */
.hljs-number,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params,
.hljs-meta,
.hljs-link {
color: #f5871f;
}
/* Tomorrow Yellow */
.hljs-attribute {
color: #eab700;
}
/* Tomorrow Green */
.hljs-string,
.hljs-symbol,
.hljs-bullet,
.hljs-addition {
color: #718c00;
}
/* Tomorrow Blue */
.hljs-title,
.hljs-section {
color: #4271ae;
}
/* Tomorrow Purple */
.hljs-keyword,
.hljs-selector-tag {
color: #8959a8;
}
.hljs {
display: block;
overflow-x: auto;
background: white;
color: #4d4d4c;
padding: 0.5em;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
+116
View File
@@ -0,0 +1,116 @@
html, body {
background-color: #fefefe;
font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
height: 100%;
margin: 0;
text-rendering: optimizeLegibility;
}
body {
display: grid;
grid-gap: 3rem;
grid-template-areas: "header" "nav" "main" "footer";
grid-template-columns: 1fr;
height: 100%;
grid-template-rows: 1fr 3rem 1fr 3rem;
}
@media screen and (max-width: 800px) {
body {
grid-template-rows: 1fr 3rem 1fr 5rem;
}
}
a {
color: #00A09D;
text-decoration: none;
}
a:hover, a:active, a:visited {
text-decoration: underline;
}
header {
grid-area: header;
display: flex;
align-items: center;
flex-direction: column;
justify-content: flex-end;
text-align: center;
padding-top: 3rem !important;
}
hgroup > p {
margin-bottom: auto;
}
nav {
grid-area: nav;
}
main {
grid-area: main;
/*justify-self: center;*/
grid-column: 1 / -1;
/*max-width: 80rem !important;*/
/*margin: 3rem 0;*/
}
footer {
grid-area: footer;
padding-bottom: 3rem !important;
}
header, nav, footer {
text-align: center;
}
article {
padding: 3rem 0;
}
article h3 {
font-size: 2rem;
font-weight: 700;
}
article.brand {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
article.design {
background-color: #875A7B;
color: #f0eeee;
}
header .homepage-logo {
margin-bottom: 2rem;
}
@media (min-width: 40rem) {
.row {
flex-direction: column;
margin-left: initial;
width: 100%;
}
.row .column {
margin-bottom: initial;
padding: initial;
}
}
@media (min-width: 60rem) {
.row {
flex-direction: row;
margin-left: -1.0rem;
width: calc(100% + 2.0rem);
}
.row .column {
margin-bottom: inherit;
padding: 0 1.0rem;
}
}
+602
View File
@@ -0,0 +1,602 @@
/*!
* Milligram v1.3.0
* https://milligram.github.io
*
* Copyright (c) 2017 CJ Patoilo
* Licensed under the MIT license
*/
*,
*:after,
*:before {
box-sizing: inherit;
}
html {
box-sizing: border-box;
font-size: 62.5%;
}
body {
color: #606c76;
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
font-size: 1.6em;
font-weight: 300;
letter-spacing: .01em;
line-height: 1.6;
}
blockquote {
border-left: 0.3rem solid #d1d1d1;
margin-left: 0;
margin-right: 0;
padding: 1rem 1.5rem;
}
blockquote *:last-child {
margin-bottom: 0;
}
.button,
button,
input[type='button'],
input[type='reset'],
input[type='submit'] {
background-color: #9b4dca;
border: 0.1rem solid #9b4dca;
border-radius: .4rem;
color: #fff;
cursor: pointer;
display: inline-block;
font-size: 1.1rem;
font-weight: 700;
height: 3.8rem;
letter-spacing: .1rem;
line-height: 3.8rem;
padding: 0 3.0rem;
text-align: center;
text-decoration: none;
text-transform: uppercase;
white-space: nowrap;
}
.button:focus, .button:hover,
button:focus,
button:hover,
input[type='button']:focus,
input[type='button']:hover,
input[type='reset']:focus,
input[type='reset']:hover,
input[type='submit']:focus,
input[type='submit']:hover {
background-color: #606c76;
border-color: #606c76;
color: #fff;
outline: 0;
}
.button[disabled],
button[disabled],
input[type='button'][disabled],
input[type='reset'][disabled],
input[type='submit'][disabled] {
cursor: default;
opacity: .5;
}
.button[disabled]:focus, .button[disabled]:hover,
button[disabled]:focus,
button[disabled]:hover,
input[type='button'][disabled]:focus,
input[type='button'][disabled]:hover,
input[type='reset'][disabled]:focus,
input[type='reset'][disabled]:hover,
input[type='submit'][disabled]:focus,
input[type='submit'][disabled]:hover {
background-color: #9b4dca;
border-color: #9b4dca;
}
.button.button-outline,
button.button-outline,
input[type='button'].button-outline,
input[type='reset'].button-outline,
input[type='submit'].button-outline {
background-color: transparent;
color: #9b4dca;
}
.button.button-outline:focus, .button.button-outline:hover,
button.button-outline:focus,
button.button-outline:hover,
input[type='button'].button-outline:focus,
input[type='button'].button-outline:hover,
input[type='reset'].button-outline:focus,
input[type='reset'].button-outline:hover,
input[type='submit'].button-outline:focus,
input[type='submit'].button-outline:hover {
background-color: transparent;
border-color: #606c76;
color: #606c76;
}
.button.button-outline[disabled]:focus, .button.button-outline[disabled]:hover,
button.button-outline[disabled]:focus,
button.button-outline[disabled]:hover,
input[type='button'].button-outline[disabled]:focus,
input[type='button'].button-outline[disabled]:hover,
input[type='reset'].button-outline[disabled]:focus,
input[type='reset'].button-outline[disabled]:hover,
input[type='submit'].button-outline[disabled]:focus,
input[type='submit'].button-outline[disabled]:hover {
border-color: inherit;
color: #9b4dca;
}
.button.button-clear,
button.button-clear,
input[type='button'].button-clear,
input[type='reset'].button-clear,
input[type='submit'].button-clear {
background-color: transparent;
border-color: transparent;
color: #9b4dca;
}
.button.button-clear:focus, .button.button-clear:hover,
button.button-clear:focus,
button.button-clear:hover,
input[type='button'].button-clear:focus,
input[type='button'].button-clear:hover,
input[type='reset'].button-clear:focus,
input[type='reset'].button-clear:hover,
input[type='submit'].button-clear:focus,
input[type='submit'].button-clear:hover {
background-color: transparent;
border-color: transparent;
color: #606c76;
}
.button.button-clear[disabled]:focus, .button.button-clear[disabled]:hover,
button.button-clear[disabled]:focus,
button.button-clear[disabled]:hover,
input[type='button'].button-clear[disabled]:focus,
input[type='button'].button-clear[disabled]:hover,
input[type='reset'].button-clear[disabled]:focus,
input[type='reset'].button-clear[disabled]:hover,
input[type='submit'].button-clear[disabled]:focus,
input[type='submit'].button-clear[disabled]:hover {
color: #9b4dca;
}
code {
background: #f4f5f6;
border-radius: .4rem;
font-size: 86%;
margin: 0 .2rem;
padding: .2rem .5rem;
white-space: nowrap;
}
pre {
background: #f4f5f6;
border-left: 0.3rem solid #9b4dca;
overflow-y: hidden;
}
pre > code {
border-radius: 0;
display: block;
padding: 1rem 1.5rem;
white-space: pre;
}
hr {
border: 0;
border-top: 0.1rem solid #f4f5f6;
margin: 3.0rem 0;
}
input[type='email'],
input[type='number'],
input[type='password'],
input[type='search'],
input[type='tel'],
input[type='text'],
input[type='url'],
textarea,
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: transparent;
border: 0.1rem solid #d1d1d1;
border-radius: .4rem;
box-shadow: none;
box-sizing: inherit;
height: 3.8rem;
padding: .6rem 1.0rem;
width: 100%;
}
input[type='email']:focus,
input[type='number']:focus,
input[type='password']:focus,
input[type='search']:focus,
input[type='tel']:focus,
input[type='text']:focus,
input[type='url']:focus,
textarea:focus,
select:focus {
border-color: #9b4dca;
outline: 0;
}
select {
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;
padding-right: 3.0rem;
}
select:focus {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#9b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>');
}
textarea {
min-height: 6.5rem;
}
label,
legend {
display: block;
font-size: 1.6rem;
font-weight: 700;
margin-bottom: .5rem;
}
fieldset {
border-width: 0;
padding: 0;
}
input[type='checkbox'],
input[type='radio'] {
display: inline;
}
.label-inline {
display: inline-block;
font-weight: normal;
margin-left: .5rem;
}
.container {
margin: 0 auto;
max-width: 112.0rem;
padding: 0 2.0rem;
position: relative;
width: 100%;
}
.row {
display: flex;
flex-direction: column;
padding: 0;
width: 100%;
}
.row.row-no-padding {
padding: 0;
}
.row.row-no-padding > .column {
padding: 0;
}
.row.row-wrap {
flex-wrap: wrap;
}
.row.row-top {
align-items: flex-start;
}
.row.row-bottom {
align-items: flex-end;
}
.row.row-center {
align-items: center;
}
.row.row-stretch {
align-items: stretch;
}
.row.row-baseline {
align-items: baseline;
}
.row .column {
display: block;
flex: 1 1 auto;
margin-left: 0;
max-width: 100%;
width: 100%;
}
.row .column.column-offset-10 {
margin-left: 10%;
}
.row .column.column-offset-20 {
margin-left: 20%;
}
.row .column.column-offset-25 {
margin-left: 25%;
}
.row .column.column-offset-33, .row .column.column-offset-34 {
margin-left: 33.3333%;
}
.row .column.column-offset-50 {
margin-left: 50%;
}
.row .column.column-offset-66, .row .column.column-offset-67 {
margin-left: 66.6666%;
}
.row .column.column-offset-75 {
margin-left: 75%;
}
.row .column.column-offset-80 {
margin-left: 80%;
}
.row .column.column-offset-90 {
margin-left: 90%;
}
.row .column.column-10 {
flex: 0 0 10%;
max-width: 10%;
}
.row .column.column-20 {
flex: 0 0 20%;
max-width: 20%;
}
.row .column.column-25 {
flex: 0 0 25%;
max-width: 25%;
}
.row .column.column-33, .row .column.column-34 {
flex: 0 0 33.3333%;
max-width: 33.3333%;
}
.row .column.column-40 {
flex: 0 0 40%;
max-width: 40%;
}
.row .column.column-50 {
flex: 0 0 50%;
max-width: 50%;
}
.row .column.column-60 {
flex: 0 0 60%;
max-width: 60%;
}
.row .column.column-66, .row .column.column-67 {
flex: 0 0 66.6666%;
max-width: 66.6666%;
}
.row .column.column-75 {
flex: 0 0 75%;
max-width: 75%;
}
.row .column.column-80 {
flex: 0 0 80%;
max-width: 80%;
}
.row .column.column-90 {
flex: 0 0 90%;
max-width: 90%;
}
.row .column .column-top {
align-self: flex-start;
}
.row .column .column-bottom {
align-self: flex-end;
}
.row .column .column-center {
-ms-grid-row-align: center;
align-self: center;
}
@media (min-width: 40rem) {
.row {
flex-direction: row;
margin-left: -1.0rem;
width: calc(100% + 2.0rem);
}
.row .column {
margin-bottom: inherit;
padding: 0 1.0rem;
}
}
a {
color: #9b4dca;
text-decoration: none;
}
a:focus, a:hover {
color: #606c76;
}
dl,
ol,
ul {
list-style: none;
margin-top: 0;
padding-left: 0;
}
dl dl,
dl ol,
dl ul,
ol dl,
ol ol,
ol ul,
ul dl,
ul ol,
ul ul {
font-size: 90%;
margin: 1.5rem 0 1.5rem 3.0rem;
}
ol {
list-style: decimal inside;
}
ul {
list-style: circle inside;
}
.button,
button,
dd,
dt,
li {
margin-bottom: 1.0rem;
}
fieldset,
input,
select,
textarea {
margin-bottom: 1.5rem;
}
blockquote,
dl,
figure,
form,
ol,
p,
pre,
table,
ul {
margin-bottom: 2.5rem;
}
table {
border-spacing: 0;
width: 100%;
}
td,
th {
border-bottom: 0.1rem solid #e1e1e1;
padding: 1.2rem 1.5rem;
text-align: left;
}
td:first-child,
th:first-child {
padding-left: 0;
}
td:last-child,
th:last-child {
padding-right: 0;
}
b,
strong {
font-weight: bold;
}
p {
margin-top: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 300;
letter-spacing: -.1rem;
margin-bottom: 2.0rem;
margin-top: 0;
}
h1 {
font-size: 4.6rem;
line-height: 1.2;
}
h2 {
font-size: 3.6rem;
line-height: 1.25;
}
h3 {
font-size: 2.8rem;
line-height: 1.3;
}
h4 {
font-size: 2.2rem;
letter-spacing: -.08rem;
line-height: 1.35;
}
h5 {
font-size: 1.8rem;
letter-spacing: -.05rem;
line-height: 1.5;
}
h6 {
font-size: 1.6rem;
letter-spacing: 0;
line-height: 1.4;
}
img {
max-width: 100%;
}
.clearfix:after {
clear: both;
content: ' ';
display: table;
}
.float-left {
float: left;
}
.float-right {
float: right;
}
/*# sourceMappingURL=milligram.css.map */
+349
View File
@@ -0,0 +1,349 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+15
View File
@@ -0,0 +1,15 @@
import { mount, Component, useState, loadFile } from "@odoo/owl";
class Counter extends Component {
static template = "Counter";
setup() {
this.state = useState({ value: 0 });
}
increment() {
this.state.value++;
}
}
const templates = await loadFile("./counter.xml");
mount(Counter, document.getElementById("app-container"), { templates });
+7
View File
@@ -0,0 +1,7 @@
<templates>
<t t-name="Counter">
<button t-on-click="increment">
Click Me! [<t t-esc="state.value"/>]
</button>
</t>
</templates>
+9
View File
@@ -0,0 +1,9 @@
import { loadFile } from "@odoo/owl";
for (const [className, type] of [["xml", "xml"], ["javascript", "js"]]) {
loadFile(`./counter.${type}`).then(code => {
const el = document.querySelector(`code.${className}`);
el.textContent = code;
hljs.highlightBlock(el);
})
}
+75
View File
@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OWL Framework</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
<link rel="stylesheet" href="assets/normalize.css">
<link rel="stylesheet" href="assets/milligram.css">
<link rel="stylesheet" href="assets/highlight.tomorrow.css">
<link rel="stylesheet" href="assets/main.css">
<script src="./owl.js"></script>
</head>
<body>
<header class="container">
<img class="homepage-logo" src="assets/owl_1f989.png" alt="OWL Logo">
<hgroup>
<h1>OWL Framework</h1>
<p>Mysterious OWL: A web framework for structured, dynamic and maintainable applications</p>
</hgroup>
</header>
<nav class="container">
<p>
<a href="https://github.com/odoo/owl" title="https://github.com/odoo/owl">Github</a>
&bull;
<a href="https://github.com/odoo/owl/blob/master/doc/readme.md">Documentation</a>
&bull;
<a href="playground/">Playground</a>
</p>
</nav>
<main>
<article class="design">
<div class="container">
<h2>Why OWL?</h2>
<div class="row">
<section class="column">
<h3>XML based</h3>
<p>Templates are based on the XML format, which allows interesting applications. For example, they could be stored in a database and modified dynamically with <tt>xpaths</tt>.</p>
</section>
<section class="column">
<h3>Templates compiled in the browser</h3>
<p>This may not be a good fit for all applications, but if you need to generate dynamically user interfaces in the browser, this is very powerful. For example, a generic form view component could generate a specific form user interface for each various models, from a XML view.</p>
</section>
<section class="column">
<h3>No toolchain required</h3>
<p>This is extremely useful for some applications, if, for various reasons (security/deployment/dynamic modules/specific assets tools), it is not ok to use standard web tools based on <tt>npm</tt>.</p>
</section>
</div>
</div>
</article>
<article class="example">
<div class="container">
<h2>Example</h2>
<div class="row">
<section class="column">
<pre><code class="javascript"></code></pre>
</section>
<section class="column">
<pre><code class="xml"></code></pre>
<div id="app-container"></div>
</section>
</div>
</div>
</article>
</main>
<footer>
<p><a href=".">OWL</a> is licensed under LGPLv3.<br>Logo from <a href="https://github.com/googlefonts/noto-emoji">Google Noto Emoji Font</a>, licensed under Apache License 2.0</p>
</footer>
<script src="assets/highlight.pack.js"></script>
<script type="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
<script type="module" src="display_code.js"></script>
<script type="module" src="counter.js"></script>
</body>
</html>
+5925
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL Playground</title>
<link rel="icon" href="data:,">
<script src="libs/ace.js" type="text/javascript" charset="utf-8"></script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/solid.css" integrity="sha384-QokYePQSOwpBDuhlHOsX0ymF6R/vLk/UQVz3WHa6wygxI5oGTmDTv8wahFOSspdm" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/fontawesome.css" integrity="sha384-vd1e11sR28tEK9YANUtpIOdjGW14pS87bUBuOIoBILVWLFnS+MCX9T6MMf0VdPGq" crossorigin="anonymous">
<!-- Application JS/CSS -->
<link rel="stylesheet" href="playground.css">
<script type="module" src="playground.js"></script>
</head>
<body>
</body>
</html>
@@ -20,7 +20,7 @@ body {
display: grid;
height: 100%;
width: 100%;
grid-template-columns: auto 8px 100%;
grid-template-columns: auto 8px 1fr;
}
/* LEFT BAR ****************************************/
@@ -1,7 +1,19 @@
import { SAMPLES } from "./samples.js";
import { debounce, loadJS } from "./utils.js";
import { exportStandaloneApp } from "./export_snippet.js";
const { useState, useRef, onMounted, onWillUnmount, onPatched, onWillUpdateProps } = owl;
import {
App,
Component,
useState,
useRef,
onMounted,
onWillUnmount,
onPatched,
onWillUpdateProps,
loadFile as _loadFile,
whenReady,
__info__,
useEffect,
onWillStart,
} from "../owl.js";
//------------------------------------------------------------------------------
// Constants, helpers, utils
@@ -16,40 +28,38 @@ const MODES = {
const DEFAULT_XML = `<templates>
</templates>`;
// Memoize loadFile so that samples aren't reloaded whenever a sample is selected
const fileCache = {};
const loadFile = (path) => {
if (!(path in fileCache)) {
fileCache[path] = _loadFile(path);
}
return fileCache[path];
}
/**
* Make an iframe, with all the js, css and xml properly injected.
*/
function makeCodeIframe(js, css, xml) {
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "").replace(/`/g, '\\\`');
// escape backticks in the xml so they don't close the template string
const escapedXml = xml.replace(/`/g, '\\\`');
// create iframe
const iframe = document.createElement("iframe");
iframe.onload = () => {
const doc = iframe.contentDocument;
const utilsMod = doc.createElement("script");
utilsMod.setAttribute("type", "module");
utilsMod.setAttribute("src", "utils.js");
doc.head.appendChild(utilsMod);
// inject js
const owlScript = doc.createElement("script");
owlScript.type = "text/javascript";
owlScript.src = "../owl.js";
owlScript.addEventListener("load", () => {
const script = doc.createElement("script");
script.type = "module";
const content = `
(async function(TEMPLATES) {
${js}
})(\`${sanitizedXML}\`)`;
script.innerHTML = content;
doc.body.appendChild(script);
});
doc.head.appendChild(owlScript);
// inject css
const importMap = doc.createElement("script");
importMap.type = "importmap";
importMap.textContent = `{ "imports": { "@odoo/owl": "../owl.js" } }`;
doc.head.appendChild(importMap);
const script = doc.createElement("script");
script.type = "module";
script.textContent = `const TEMPLATES = \`${escapedXml}\`\n${js}`;
doc.body.appendChild(script);
const style = document.createElement("style");
style.innerHTML = css;
style.innerText = css;
doc.head.appendChild(style);
};
return iframe;
@@ -58,53 +68,84 @@ function makeCodeIframe(js, css, xml) {
//------------------------------------------------------------------------------
// SAMPLES
//------------------------------------------------------------------------------
const SAMPLES = [
{
description: "Components",
folder: "components",
code: ["js", "xml", "css"],
},
{
description: "Form Input Bindings",
folder: "form",
code: ["js", "xml"],
},
{
description: "Inline templates",
folder: "single_file_component",
code: ["js"],
},
{
description: "Lifecycle demo",
folder: "lifecycle_demo",
code: ["js", "xml", "css"],
},
{
description: "Customized hook",
folder: "custom_hooks",
code: ["js", "xml", "css"],
},
{
description: "Todo List App (with reactivity)",
folder: "todo_app",
code: ["js", "xml", "css"],
},
{
description: "Responsive app",
folder: "responsive_app",
code: ["js", "xml", "css"],
},
{
description: "Slots And Generic Components",
folder: "slots",
code: ["js", "xml", "css"],
},
{
description: "Window Management System",
folder: "window_manager",
code: ["js", "xml", "css"],
},
{
description: "Benchmark example",
folder: "benchmark",
code: ["js", "xml", "css"],
},
]
function loadSamples() {
let result = SAMPLES.slice();
const result = SAMPLES.map(({ description, folder, code }) => ({
description,
code: async () => Object.fromEntries(
await Promise.all(
code.map(async (type) => [type, await loadFile(`./samples/${folder}/${folder}.${type}`)])
)
),
}));
const localSample = localStorage.getItem("owl-playground-local-sample");
if (localSample) {
const { js, css, xml } = JSON.parse(localSample);
result.unshift({
description: "Local Storage Code",
code: js,
xml,
css
code: () => Promise.resolve({ js, xml, css }),
});
}
return result;
}
function saveLocalSample(js, css, xml) {
const str = JSON.stringify({ js, css, xml });
localStorage.setItem("owl-playground-local-sample", str);
}
function deleteLocalSample() {
localStorage.removeItem("owl-playground-local-sample");
}
function useSamples() {
const samples = loadSamples();
const component = owl.useComponent();
let interval;
onMounted(() => {
const state = component.state;
interval = setInterval(() => {
if (component.isDirty) {
saveLocalSample(state.js, state.css, state.xml);
}
}, 1000);
});
onWillUnmount(() => {
clearInterval(interval);
});
return samples;
}
//------------------------------------------------------------------------------
// Tabbed editor
//------------------------------------------------------------------------------
class TabbedEditor extends owl.Component {
class TabbedEditor extends Component {
setup() {
const props = this.props;
this.state = useState({
@@ -168,7 +209,7 @@ class TabbedEditor extends owl.Component {
const resizer = ev => {
const delta = ev.clientY - y;
y = ev.clientY;
this.trigger("updatePanelHeight", { delta });
this.props.updatePanelHeight({ delta });
};
document.body.addEventListener("mousemove", resizer);
document.body.addEventListener("mouseup", () => {
@@ -205,36 +246,53 @@ TabbedEditor.template = "TabbedEditor";
//------------------------------------------------------------------------------
// MAIN APP
//------------------------------------------------------------------------------
class App extends owl.Component {
class Playground extends Component {
static template = "Playground";
static components = { TabbedEditor };
setup() {
this.version = owl.__info__.version;
this.SAMPLES = useSamples();
this.isDirty = false;
this.version = __info__.version;
this.isDirty = false;
this.state = useState({
js: this.SAMPLES[0].code,
css: this.SAMPLES[0].css || "",
xml: this.SAMPLES[0].xml || DEFAULT_XML,
js: "",
css: "",
xml: DEFAULT_XML,
displayWelcome: true,
splitLayout: true,
leftPaneWidth: Math.ceil(window.innerWidth / 2),
topPanelHeight: null
});
this.toggleLayout = debounce(this.toggleLayout, 250, true);
this.runCode = debounce(this.runCode, 250, true);
this.downloadCode = debounce(this.downloadCode, 250, true);
this.content = useRef("content");
this.updateCode = this.updateCode.bind(this);
this.samples = loadSamples();
if (window.location.hash) {
try {
const { js, css, xml } = JSON.parse(atob(decodeURIComponent(window.location.hash.slice(1))));
if (![js, css, xml].every(item => typeof item === "string")) {
return;
if ([js, css, xml].every(item => typeof item === "string")) {
Object.assign(this.state, { js, css, xml });
}
Object.assign(this.state, { js, css, xml });
} catch {}
}
onWillStart(async () => {
if (!this.state.js) {
this.setSample(await this.samples[0].code());
}
})
useEffect(() => {
const interval = setInterval(() => {
if (this.isDirty) {
const { js, css, xml } = this.state;
const str = JSON.stringify({ js, css, xml });
localStorage.setItem("owl-playground-local-sample", str);
}
}, 1000);
return () => clearInterval(interval);
}, () => []);
this.toggleLayout = debounce(this.toggleLayout, 250, true);
this.runCode = debounce(this.runCode, 250, true);
this.exportStandaloneApp = debounce(this.exportStandaloneApp, 250, true);
this.content = useRef("content");
this.updateCode = this.updateCode.bind(this);
}
runCode() {
@@ -258,12 +316,11 @@ class App extends owl.Component {
window.location.href = link.href;
}
setSample(ev) {
const sample = this.SAMPLES.find(s => s.description === ev.target.value);
this.state.js = sample.code;
setSample(sample) {
this.state.js = sample.js;
this.state.css = sample.css || "";
this.state.xml = sample.xml || DEFAULT_XML;
deleteLocalSample();
localStorage.removeItem("owl-playground-local-sample");
this.isDirty = false;
}
@@ -271,14 +328,14 @@ class App extends owl.Component {
return `width:${this.state.leftPaneWidth}px`;
}
get rightPaneStyle() {
return `width:${window.innerWidth - 6 - this.state.leftPaneWidth}px`;
}
get topEditorStyle() {
return `flex: 0 0 ${this.state.topPanelHeight}px`;
}
async onSampleChange(ev) {
this.setSample(await this.samples.find(s => s.description === ev.target.value).code());
}
onMouseDown() {
const resizer = ev => {
this.state.leftPaneWidth = ev.clientX;
@@ -305,39 +362,40 @@ class App extends owl.Component {
toggleLayout() {
this.state.splitLayout = !this.state.splitLayout;
}
updatePanelHeight(ev) {
if (!ev.detail.delta) {
return;
}
let height = this.state.topPanelHeight;
if (!height) {
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
}
this.state.topPanelHeight = height + ev.detail.delta;
updatePanelHeight({ delta }) {
let height = this.state.topPanelHeight || document.querySelector(".tabbed-editor").clientHeight;
this.state.topPanelHeight = height + delta;
}
async downloadCode() {
async exportStandaloneApp() {
const { js, css, xml } = this.state;
const content = await exportStandaloneApp(js, css, xml);
await loadJS("libs/jszip.min.js");
const zip = new JSZip();
zip.file("app.py", await loadFile("./standalone_app/app.py"));
zip.file("index.html", await loadFile("./standalone_app/index.html"));
zip.file("owl.js", await loadFile("../owl.js"));
zip.file("app.js", `const TEMPLATES = await (await fetch('app.xml')).text();\n${js}`);
zip.file("app.css", css);
zip.file("app.xml", xml);
await loadJS("libs/FileSaver.min.js");
saveAs(content, "app.zip");
saveAs(await zip.generateAsync({ type: "blob" }), "app.zip");
}
}
App.components = { TabbedEditor };
App.template = "App";
//------------------------------------------------------------------------------
// Application initialization
//------------------------------------------------------------------------------
async function start() {
document.title = `${document.title} (v${owl.__info__.version})`;
const commit = `https://github.com/odoo/owl/commit/${owl.__info__.hash}`;
document.title = `${document.title} (v${__info__.version})`;
const commit = `https://github.com/odoo/owl/commit/${__info__.hash}`;
console.info(`This application is using Owl built with the following commit:`, commit);
const [templates] = await Promise.all([
owl.loadFile("templates.xml"),
owl.whenReady()
loadFile("templates.xml"),
whenReady()
]);
const rootApp = new owl.App(App);
const rootApp = new App(Playground, { name: "Owl Playground" });
rootApp.addTemplates(templates);
await rootApp.mount(document.body);
@@ -0,0 +1,11 @@
tr.danger {
font-weight: bold;
}
.remove:hover {
font-weight: bold;
}
.remove {
cursor: pointer;
}
@@ -0,0 +1,124 @@
import { Component, mount, useState, onPatched} from "@odoo/owl";
// -----------------------------------------------------------------------------
// Data generation
// -----------------------------------------------------------------------------
let idCounter = 1;
const adjectives = [
"pretty", "large", "big", "small", "tall", "short", "long", "handsome", "plain",
"quaint", "clean", "elegant", "easy", "angry", "crazy", "helpful", "mushy", "odd",
"unsightly", "adorable", "important", "inexpensive", "cheap", "expensive", "fancy"];
const colours = ["red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange"];
const nouns = ["table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", "mouse", "keyboard"];
function _random (max) { return Math.round(Math.random() * 1000) % max; };
function buildData(count) {
const data = new Array(count);
for (let i = 0; i < count; i++) {
const label = `${adjectives[_random(adjectives.length)]} ${colours[_random(colours.length)]} ${nouns[_random(nouns.length)]}`;
data[i] = {
id: idCounter++,
label,
};
}
return data;
}
// -----------------------------------------------------------------------------
// Components
// -----------------------------------------------------------------------------
class Button extends Component {
static template = "Button";
}
class Row extends Component {
static template = "Row";
}
class Root extends Component {
static template = "Root";
static components = { Button, Row };
setup() {
this.state = useState({
rows: [],
selectedRowId: null
});
this.benchmarking = false;
onPatched(() => {
if (this.benchmarking) {
this.stop();
}
});
}
start(descr) {
this.benchmarking = `[${descr}]`;
console.time(this.benchmarking);
}
stop() {
console.timeEnd(this.benchmarking);
this.benchmarking = false;
}
run() {
this.start('add1000');
this.state.rows = buildData(1000);
this.state.selectedRowId = null;
}
runLots() {
this.start('add10_000');
this.state.rows = buildData(10_000);
this.state.selectedRowId = null;
}
add() {
this.start('append1000');
this.state.rows = this.state.rows.concat(buildData(1000));
}
update() {
this.start('update1/10th');
let index = 0;
const rows = this.state.rows;
while (index < rows.length) {
rows[index].label = rows[index].label + " !!!";
index += 10;
}
}
clear() {
this.start('clear');
this.state.rows = [];
this.state.selectedRowId = null;
}
swapRows() {
this.start('swap');
const rows = this.state.rows;
if (rows.length > 998) {
let tmp = rows[1];
rows[1] = rows[998];
rows[998] = tmp;
}
}
selectRow(id) {
this.start('select');
this.state.selectedRowId = id;
}
removeRow(id) {
this.start('remove1');
const rows = this.state.rows;
rows.splice(rows.findIndex(row => row.id === id), 1);
}
}
// dev=false for benchmarking. we don't want to benchmark dev code!
mount(Root, document.body, { templates: TEMPLATES, dev: false });
@@ -0,0 +1,52 @@
<templates>
<t t-name="Button">
<div class='col-sm-6 smallpad'>
<button t-att-id="props.id" class='btn btn-primary btn-block' type='button' t-on-click="props.onClick">
<t t-esc="props.text"/>
</button>
</div>
</t>
<t t-name="Row">
<tr t-att-class="props.isSelected ? 'danger' : ''">
<td class="col-md-1" t-esc="props.row.id" />
<td class="col-md-4">
<a t-on-click="() => props.onSelect(props.row.id)" t-esc="props.row.label" />
</td>
<td class="col-md-1">
<a t-on-click="() => props.onRemove(props.row.id)" class="remove">[x]
<span class='glyphicon glyphicon-remove' aria-hidden="true" />
</a>
</td>
<td class='col-md-6'/>
</tr>
</t>
<t t-name="Root">
<div class='container'>
<div class='jumbotron'>
<div class='row'>
<div class='col-md-6'>
<h1>Owl Keyed</h1>
</div>
<div class='col-md-6'>
<div class='row'>
<Button id="'run'" onClick.bind="run" text="'Create 1,000 rows'" />
<Button id="'runlots'" onClick.bind="runLots" text="'Create 10,000 rows'" />
<Button id="'add'" onClick.bind="add" text="'Append 1,000 rows'" />
<Button id="'update'" onClick.bind="update" text="'Update every 10th row'" />
<Button id="'clear'" onClick.bind="clear" text="'Clear'" />
<Button id="'swaprows'" onClick.bind="swapRows" text="'Swap Rows'" />
</div>
</div>
</div>
</div>
<table class='table table-hover table-striped test-data'>
<tbody>
<t t-foreach="state.rows" t-as="row" t-key="row.id">
<Row row="row" isSelected="row.id === state.selectedRowId" onSelect.bind="selectRow" onRemove.bind="removeRow"/>
</t>
</tbody>
</table>
<span class='preloadicon glyphicon glyphicon-remove' aria-hidden="true" />
</div>
</t>
</templates>
@@ -0,0 +1,10 @@
.greeter {
font-size: 20px;
width: 300px;
height: 100px;
margin: 5px;
text-align: center;
line-height: 100px;
background-color: #eeeeee;
user-select: none;
}
@@ -0,0 +1,26 @@
// In this example, we show how components can be defined and created.
import { Component, useState, mount } from "@odoo/owl";
class Greeter extends Component {
static template = "Greeter";
setup() {
this.state = useState({ word: 'Hello' });
}
toggle() {
this.state.word = this.state.word === 'Hi' ? 'Hello' : 'Hi';
}
}
// Main root component
class Root extends Component {
static components = { Greeter };
static template = "Root"
setup() {
this.state = useState({ name: 'World'});
}
}
mount(Root, document.body, { templates: TEMPLATES, dev: true });
@@ -0,0 +1,9 @@
<templates>
<div t-name="Greeter" class="greeter" t-on-click="toggle">
<t t-esc="state.word"/>, <t t-esc="props.name"/>
</div>
<t t-name="Root">
<Greeter name="state.name"/>
</t>
</templates>
@@ -0,0 +1,6 @@
button {
width: 120px;
height: 35px;
font-size: 16px;
}
@@ -0,0 +1,40 @@
// In this example, we show how hooks can be used or defined.
import { Component, mount, useState, onWillDestroy } from "@odoo/owl";
// We define here a custom behaviour: this hook tracks the state of the mouse
// position
function useMouse() {
const position = useState({x:0, y: 0});
function update(e) {
position.x = e.clientX;
position.y = e.clientY;
}
window.addEventListener('mousemove', update);
onWillDestroy(() => {
window.removeEventListener('mousemove', update);
});
return position;
}
// Main root component
class Root extends Component {
static template = "Root";
setup() {
// simple state hook (reactive object)
this.counter = useState({ value: 0 });
// this hooks is bound to the 'mouse' property.
this.mouse = useMouse();
}
increment() {
this.counter.value++;
}
}
// Application setup
mount(Root, document.body, { templates: TEMPLATES, dev: true });
@@ -0,0 +1,7 @@
<templates>
<div t-name="Root">
<button t-on-click="increment">Click! <t t-esc="counter.value"/></button>
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
</div>
</templates>
+22
View File
@@ -0,0 +1,22 @@
// This example illustrate how the t-model directive can be used to synchronize
// data between html inputs (and select/textareas) and the state of a component.
// Note that there are two controls with t-model="color": they are totally
// synchronized.
import { Component, useState, mount } from "@odoo/owl";
class Form extends Component {
static template = "Form";
setup() {
this.state = useState({
text: "",
othertext: "",
number: 11,
color: "",
bool: false
});
}
}
// Application setup
mount(Form, document.body, { templates: TEMPLATES, dev: true });
+37
View File
@@ -0,0 +1,37 @@
<templates>
<div t-name="Form">
<h1>Form</h1>
<div>
Text (immediate): <input t-model="state.text"/>
</div>
<div>
Other text (lazy): <input t-model.lazy="state.othertext"/>
</div>
<div>
Number: <input t-model.number="state.number"/>
</div>
<div>
Boolean: <input type="checkbox" t-model="state.bool"/>
</div>
<div>
Color, with a select:
<select t-model="state.color">
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
</div>
<div>
Color, with radio buttons:
<span><input type="radio" name="color" id="red" value="red" t-model="state.color"/><label for="red">Red</label></span>
<span><input type="radio" name="color" id="blue" value="blue" t-model="state.color"/><label for="blue">Blue</label></span>
</div>
<hr/>
<h1>State</h1>
<div>Text: <t t-esc="state.text"/></div>
<div>Other Text: <t t-esc="state.othertext"/></div>
<div>Number: <t t-esc="state.number"/></div>
<div>Boolean: <t t-if="state.bool">True</t><t t-else="">False</t></div>
<div>Color: <t t-esc="state.color"/></div>
</div>
</templates>
+7
View File
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"owl": ["../../../src/index.ts"],
},
}
}
@@ -0,0 +1,11 @@
button {
font-size: 18px;
margin: 5px;
}
.demo {
margin: 10px;
padding: 10px;
background-color: #dddddd;
width: 250px;
}
@@ -0,0 +1,68 @@
// This example shows all the possible lifecycle hooks
//
// The root component controls a sub component (DemoComponent). It logs all its lifecycle
// methods in the console. Try modifying its state by clicking on it, or by
// clicking on the two main buttons, and look into the console to see what
// happens.
import {
Component,
useState,
mount,
useComponent,
onWillStart,
onMounted,
onWillUnmount,
onWillUpdateProps,
onPatched,
onWillPatch,
onWillRender,
onRendered,
onWillDestroy,
} from "@odoo/owl";
function useLogLifecycle() {
const component = useComponent();
const name = component.constructor.name;
onWillStart(() => console.log(`${name}:willStart`));
onMounted(() => console.log(`${name}:mounted`));
onWillUpdateProps(() => console.log(`${name}:willUpdateProps`));
onWillRender(() => console.log(`${name}:willRender`));
onRendered(() => console.log(`${name}:rendered`));
onWillPatch(() => console.log(`${name}:willPatch`));
onPatched(() => console.log(`${name}:patched`));
onWillUnmount(() => console.log(`${name}:willUnmount`));
onWillDestroy(() => console.log(`${name}:willDestroy`));
}
class DemoComponent extends Component {
static template = "DemoComponent";
setup() {
useLogLifecycle();
this.state = useState({ n: 0 });
}
increment() {
this.state.n++;
}
}
class Root extends Component {
static template = "Root";
static components = { DemoComponent };
setup() {
useLogLifecycle();
this.state = useState({ n: 0, flag: true });
}
increment() {
this.state.n++;
}
toggleSubComponent() {
this.state.flag = !this.state.flag;
}
}
mount(Root, document.body, { templates: TEMPLATES, dev: true });
@@ -0,0 +1,15 @@
<templates>
<div t-name="DemoComponent" t-on-click="increment" class="demo">
<div>Demo Sub Component</div>
<div>(click on me to update me)</div>
<div>Props: <t t-esc="props.n"/>, State: <t t-esc="state.n"/>. </div>
</div>
<div t-name="Root">
<button t-on-click="increment">Increment Parent State</button>
<button t-on-click="toggleSubComponent">Toggle SubComponent</button>
<div t-if="state.flag">
<DemoComponent n="state.n"/>
</div>
</div>
</templates>
@@ -0,0 +1,56 @@
body {
margin: 0;
}
.app {
height: 100%;
flex-direction: column;
}
.app.desktop {
display: flex;
}
.navbar {
flex: 0 0 30px;
height: 30px;
background-color: cadetblue;
color: white;
line-height: 30px;
}
.controlpanel {
flex: 0 0 100px;
height: 100px;
background-color: #dddddd;
padding: 8px;
}
.content-wrapper {
flex: 1 1 auto;
position: relative;
}
.content {
display: flex;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.formview {
overflow-y: auto;
flex: 1 1 60%;
min-height: 200px;
padding: 8px;
}
.chatter {
overflow-y: auto;
flex: 1 1 40%;
background-color: #eeeeee;
color: #333333;
padding: 8px;
}
@@ -0,0 +1,118 @@
// In this example, we show how one can design an application that is responsive:
// its UI is different in mobile mode or in desktop mode.
//
// The main idea is to have a "isMobile" key in the environment, then listen
// to resize events and update the env if needed. Then, the whole interface
// will be updated, creating and destroying components as needed.
//
// To see this in action, try resizing the window. The application will switch
// to mobile mode whenever it has less than 768px.
import { Component, useState, mount, reactive, useEnv } from "@odoo/owl";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
function debounce(func, wait, immediate) {
let timeout;
return function () {
const context = this;
const args = arguments;
function later() {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
}
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
}
//------------------------------------------------------------------------------
// Responsive hook
//------------------------------------------------------------------------------
function createUI() {
const getIsMobile = () => window.innerWidth <= 768;
const ui = reactive({ isMobile: getIsMobile() });
const updateEnv = debounce(() => {
const isMobile = getIsMobile();
if (ui.isMobile !== isMobile) {
ui.isMobile = isMobile;
}
}, 15);
window.addEventListener("resize", updateEnv);
return ui;
}
function useUI() {
const env = useEnv();
return useState(env.ui);
}
//------------------------------------------------------------------------------
// Components
//------------------------------------------------------------------------------
class Navbar extends Component {
static template = "Navbar";
}
class MobileSearchView extends Component {
static template = "MobileSearchView";
}
class ControlPanel extends Component {
static template = "ControlPanel";
static components = { MobileSearchView };
setup() {
this.ui = useUI();
}
}
class AdvancedComponent extends Component {
static template = "AdvancedComponent";
}
class FormView extends Component {
static template = "FormView";
static components = { AdvancedComponent };
setup() {
this.ui = useUI();
}
}
class Chatter extends Component {
static template = "Chatter";
setup() {
this.messages = Array.from(Array(100).keys());
}
}
class Root extends Component {
static template = "Root";
static components = { Navbar, ControlPanel, FormView, Chatter };
setup() {
this.ui = useUI();
}
}
//------------------------------------------------------------------------------
// Application Startup
//------------------------------------------------------------------------------
const env = {
ui: createUI()
};
mount(Root, document.body, { templates: TEMPLATES, env, dev: true });
@@ -0,0 +1,42 @@
<templates>
<div t-name="Navbar" class="navbar">Navbar</div>
<div t-name="ControlPanel" class="controlpanel">
<h2>Control Panel</h2>
<MobileSearchView t-if="ui.isMobile" />
</div>
<div t-name="FormView" class="formview">
<h2>Form View</h2>
<AdvancedComponent t-if="!ui.isMobile" />
</div>
<div t-name="Chatter" class="chatter">
<h2>Chatter</h2>
<t t-foreach="messages" t-as="item" t-key="item"><div>Message <t t-esc="item"/></div></t>
</div>
<div t-name="MobileSearchView">Mobile searchview</div>
<div t-name="AdvancedComponent">
This component is only created in desktop mode.
<button>Button!</button>
</div>
<t t-name="maincontent">
<FormView />
<Chatter />
</t>
<div t-name="Root" class="app" t-att-class="{mobile: ui.isMobile, desktop: !ui.isMobile}">
<Navbar/>
<ControlPanel/>
<div class="content-wrapper" t-if="!ui.isMobile">
<div class="content">
<t t-call="maincontent"/>
</div>
</div>
<t t-else="">
<t t-call="maincontent"/>
</t>
</div>
</templates>
@@ -0,0 +1,28 @@
// This example illustrates how one can write Owl components with
// inline templates.
import { Component, useState, xml, mount } from "@odoo/owl";
// Counter component
class Counter extends Component {
static template = xml`
<button t-on-click="() => state.value++">
Click! [<t t-esc="state.value"/>]
</button>`;
state = useState({ value: 0 })
}
// Root
class Root extends Component {
static template = xml`
<div>
<Counter/>
<Counter/>
</div>`;
static components = { Counter };
}
// Application setup
mount(Root, document.body, { templates: TEMPLATES, dev: true});
+43
View File
@@ -0,0 +1,43 @@
.main {
display: flex;
}
.card {
display: flex;
flex-direction: column;
background-color: #eeeeee;
width: 200px;
height: 100px;
margin: 10px;
border: 1px solid gray;
}
.card.full {
height: 100px;
}
.card.small {
height: 25px;
}
.card-title {
flex: 0 0 25px;
font-weight: bold;
background-color: darkcyan;
color: white;
padding: 2px;
}
.card-title button {
float: right;
}
.card-content {
flex: 1 1 auto;
padding: 5px;
border-top: 1px solid white;
}
.card-footer {
border-top: 1px solid white;
}
+49
View File
@@ -0,0 +1,49 @@
// We show here how slots can be used to create generic components.
// In this example, the Card component is basically only a container. It is not
// aware of its content. It just knows where it should be (with t-slot).
// The parent component define the content with t-set-slot.
//
// Note that the t-on-click event, defined in the Root template, is executed in
// the context of the Root component, even though it is inside the Card component
import { Component, useState, mount } from "@odoo/owl";
class Card extends Component {
static template = "Card";
setup() {
this.state = useState({ showContent: true });
}
toggleDisplay() {
this.state.showContent = !this.state.showContent;
}
}
class Counter extends Component {
static template = "Counter";
setup() {
this.state = useState({val: 1});
}
inc() {
this.state.val++;
}
}
// Main root component
class Root extends Component {
static template = "Root"
static components = { Card, Counter };
setup() {
this.state = useState({a: 1, b: 3});
}
inc(key, delta) {
this.state[key] += delta;
}
}
// Application setup
mount(Root, document.body, { templates: TEMPLATES, dev: true});
+33
View File
@@ -0,0 +1,33 @@
<templates>
<div t-name="Card" class="card" t-att-class="state.showContent ? 'full' : 'small'">
<div class="card-title">
<t t-esc="props.title"/><button t-on-click="toggleDisplay">Toggle</button>
</div>
<t t-if="state.showContent">
<div class="card-content" >
<t t-slot="content"/>
</div>
<div class="card-footer">
<t t-slot="footer"/>
</div>
</t>
</div>
<div t-name="Counter">
<t t-esc="state.val"/><button t-on-click="inc">Inc</button>
</div>
<div t-name="Root" class="main">
<Card title="'Title card A'">
<t t-set-slot="content">Content of card 1... [<t t-esc="state.a"/>]</t>
<t t-set-slot="footer"><button t-on-click="() => this.inc('a', 1)">Increment A</button></t>
</Card>
<Card title="'Title card B'">
<t t-set-slot="content">
<div>Card 2... [<t t-esc="state.b"/>]</div>
<Counter />
</t>
<t t-set-slot="footer"><button t-on-click="() => this.inc('b', -1)">Decrement B</button></t>
</Card>
</div>
</templates>
@@ -0,0 +1,379 @@
html,body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
:focus {
outline: 0;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
border: 0;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
.toggle-all {
width: 1px;
height: 1px;
border: none; /* Mobile Safari */
opacity: 0;
position: absolute;
right: 100%;
bottom: 100%;
}
.toggle-all + label {
width: 60px;
height: 34px;
font-size: 0;
position: absolute;
top: -52px;
left: -13px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.toggle-all + label:before {
content: "";
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked + label:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: calc(100% - 43px);
padding: 12px 16px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle {
opacity: 0;
}
.todo-list li .toggle + label {
/*
Firefox requires \`#\` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the \`#\` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
*/
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center left;
}
.todo-list li .toggle:checked + label {
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E");
}
.todo-list li label {
word-break: break-all;
padding: 15px 15px 15px 60px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: "×";
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: "";
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio: 0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}
.filters a {
cursor: pointer;
}
@@ -0,0 +1,182 @@
// This example is an implementation of the TodoList application, from the
// www.todomvc.com project. This is a non trivial application with some
// interesting user interactions. It uses the local storage for persistence.
//
// In this implementation, we use the owl reactivity mechanism.
import { Component, useState, mount, useRef, reactive, useEnv, useEffect } from "@odoo/owl";
//------------------------------------------------------------------------------
// Constants, helpers
//------------------------------------------------------------------------------
const ENTER_KEY = 13;
const ESC_KEY = 27;
function useAutofocus(name) {
let ref = useRef(name);
useEffect(el => el && el.focus(), () => [ref.el]);
}
function useStore() {
const env = useEnv();
return useState(env.store);
}
//------------------------------------------------------------------------------
// Task store
//------------------------------------------------------------------------------
class TaskList {
constructor(tasks) {
this.tasks = tasks || [];
const taskIds = this.tasks.map((t) => t.id);
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
}
addTask(text) {
text = text.trim();
if (text) {
const task = {
id: this.nextId++,
text: text,
isCompleted: false,
};
this.tasks.push(task);
}
}
toggleTask(task) {
task.isCompleted = !task.isCompleted;
}
toggleTask(id) {
const task = this.tasks.find(t => t.id === id);
task.isCompleted = !task.isCompleted;
}
toggleAll(value) {
for (let task of this.tasks) {
task.isCompleted = value;
}
}
clearCompleted() {
const tasks = this.tasks.filter(t => t.isCompleted);
for (let task of tasks) {
this.deleteTask(task);
}
}
deleteTask(id) {
const index = this.tasks.findIndex((t) => t.id === id);
this.tasks.splice(index, 1);
}
updateTask(id, text) {
const value = text.trim();
if (!value) {
this.deleteTask(id);
} else {
const task = this.tasks.find(t => t.id === id);
task.text = value;
}
}
}
function createTaskStore() {
const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
const taskStore = reactive(new TaskList(initialTasks), saveTasks);
saveTasks();
return taskStore;
}
//------------------------------------------------------------------------------
// Todo
//------------------------------------------------------------------------------
class Todo extends Component {
static template = "Todo";
setup() {
useAutofocus("input");
this.store = useStore();
this.state = useState({
isEditing: false
});
}
handleKeyup(ev) {
if (ev.keyCode === ENTER_KEY) {
this.updateText(ev.target.value);
}
if (ev.keyCode === ESC_KEY) {
ev.target.value = this.props.text;
this.state.isEditing = false;
}
}
handleBlur(ev) {
this.updateText(ev.target.value);
}
updateText(text) {
this.store.updateTask(this.props.id, text);
this.state.isEditing = false;
}
}
//------------------------------------------------------------------------------
// TodoList
//------------------------------------------------------------------------------
class TodoList extends Component {
static template = "TodoList";
static components = { Todo };
setup() {
this.store = useStore();
this.state = useState({ filter: "all" });
}
get displayedTasks() {
const tasks = this.store.tasks;
switch (this.state.filter) {
case "active":
return tasks.filter((t) => !t.isCompleted);
case "completed":
return tasks.filter((t) => t.isCompleted);
case "all":
return tasks;
}
}
get allChecked() {
return this.store.tasks.every(todo => todo.isCompleted);
}
get remaining() {
return this.store.tasks.filter(todo => !todo.isCompleted).length;
}
get remainingText() {
const items = this.remaining < 2 ? "item" : "items";
return ` ${items} left`;
}
addTodo(ev) {
if (ev.keyCode === ENTER_KEY) {
const text = ev.target.value;
if (text.trim()) {
this.store.addTask(text);
}
ev.target.value = "";
}
}
setFilter(filter) {
this.state.filter = filter;
}
}
//------------------------------------------------------------------------------
// App Initialization
//------------------------------------------------------------------------------
const env = { store: createTaskStore() };
mount(TodoList, document.body, { env, templates: TEMPLATES, dev: true });
@@ -0,0 +1,50 @@
<templates>
<section t-name="TodoList" class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
</header>
<section class="main" t-if="store.tasks.length">
<input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="() => store.toggleAll(!allChecked)"/>
<label for="toggle-all"></label>
<ul class="todo-list">
<t t-foreach="displayedTasks" t-as="todo" t-key="todo.id">
<Todo id="todo.id" isCompleted="todo.isCompleted" text="todo.text"/>
</t>
</ul>
</section>
<footer class="footer" t-if="store.tasks.length">
<span class="todo-count">
<strong>
<t t-esc="remaining"/>
</strong>
<t t-esc="remainingText"/>
</span>
<ul class="filters">
<li>
<a t-on-click="() => this.setFilter('all')" t-att-class="{selected: state.filter === 'all'}">All</a>
</li>
<li>
<a t-on-click="() => this.setFilter('active')" t-att-class="{selected: state.filter === 'active'}">Active</a>
</li>
<li>
<a t-on-click="() => this.setFilter('completed')" t-att-class="{selected: state.filter === 'completed'}">Completed</a>
</li>
</ul>
<button class="clear-completed" t-if="store.tasks.length gt remaining" t-on-click="() => store.clearCompleted()">
Clear completed
</button>
</footer>
</section>
<li t-name="Todo" class="todo" t-att-class="{completed: props.isCompleted, editing: state.isEditing}">
<div class="view">
<input class="toggle" type="checkbox" t-on-change="() => store.toggleTask(props.id)" t-att-checked="props.completed"/>
<label t-on-dblclick="() => state.isEditing = true">
<t t-esc="props.text"/>
</label>
<button class="destroy" t-on-click="() => store.deleteTask(props.id)"></button>
</div>
<input class="edit" t-ref="input" t-if="state.isEditing" t-att-value="props.text" t-on-keyup="handleKeyup" t-on-blur="handleBlur"/>
</li>
</templates>
@@ -0,0 +1,70 @@
body {
margin: 0;
}
.app {
width: 100%;
height: 100%;
display: grid;
grid-template-rows: auto 50px;
}
.window-manager {
position: relative;
width: 100%;
height: 100%;
background-color: #eeeeee;
overflow: hidden;
}
.menubar {
background-color: #875a7b;
color: white;
}
.menubar button {
height: 40px;
font-size: 18px;
margin: 5px;
}
.window {
display: grid;
grid-template-rows: 30px auto;
border: 1px solid gray;
background-color: white;
position: absolute;
box-shadow: 1px 1px 2px 1px grey;
}
.window.dragging {
opacity: 0.75;
}
.window .header {
background-color: #875a7b;
display: grid;
grid-template-columns: auto 24px;
color: white;
line-height: 30px;
padding-left: 5px;
cursor: default;
user-select: none;
}
.window .header .close {
cursor: pointer;
font-size: 22px;
padding-left: 4px;
padding-right: 4px;
font-weight: bold;
}
.counter {
font-size: 20px;
}
.counter button {
width: 80px;
height:40px;
font-size: 20px;
}
@@ -0,0 +1,192 @@
// This example is slightly more complex than usual. We demonstrate
// here a way to manage sub windows in Owl, declaratively. This is still just a
// demonstration. Managing windows can be as complex as we want. For example,
// we could implement the following features:
// - resizing windows
// - minimizing windows
// - configuration options for windows to make a window non resizeable
// - minimal width/height
// - better heuristic for initial window position
// - ...
import { Component, useState, mount, useRef, reactive, useEnv, onMounted } from "@odoo/owl";
// -----------------------------------------------------------------------------
// Window manager code
// -----------------------------------------------------------------------------
class WindowManager {
// contains all components with metadata
static Windows = {};
windows = {}; // mapping id => info
nextId = 1;
add(type) {
const Comp = WindowManager.Windows[type];
const left = 50 + Math.round(Math.random()*(window.innerWidth - 50 - Comp.defaultWidth));
const top = 50 + Math.round(Math.random()*(window.innerHeight - 100 - Comp.defaultHeight));
const id = this.nextId++;
this.windows[id] = {
id,
title: Comp.defaultTitle,
width: Comp.defaultWidth,
height: Comp.defaultHeight,
left,
top,
Component: Comp,
};
}
close(id) {
delete this.windows[id];
}
updatePosition(id, left, top) {
const w = this.windows[id];
w.left = left;
w.top = top;
}
getWindows() {
return Object.values(this.windows);
}
}
function createWindowService() {
return reactive(new WindowManager());
}
function useWindowService() {
const env = useEnv();
return useState(env.windowService);
}
// -----------------------------------------------------------------------------
// Generic Window Component
// -----------------------------------------------------------------------------
class Window extends Component {
static template = "Window";
static nextZIndex = 1;
zIndex = 0;
setup() {
this.windowService = useWindowService();
this.root = useRef('root');
onMounted(this.updateZIndex);
}
get style() {
let { width, height, top, left } = this.props.info;
return `width: ${width}px;height: ${height}px;top:${top}px;left:${left}px;z-index:${this.zIndex}`;
}
close() {
this.windowService.close(this.props.info.id);
}
startDragAndDrop(ev) {
this.updateZIndex();
const self = this;
const root = this.root;
const el = root.el;
el.classList.add('dragging');
const current = this.props.info;
const offsetX = current.left - ev.pageX;
const offsetY = current.top - ev.pageY;
let left, top;
window.addEventListener("mousemove", moveWindow);
window.addEventListener("mouseup", stopDnD, { once: true });
function moveWindow(ev) {
left = Math.max(offsetX + ev.pageX, 0);
top = Math.max(offsetY + ev.pageY, 0);
el.style.left = `${left}px`;
el.style.top = `${top}px`;
}
function stopDnD() {
window.removeEventListener("mousemove", moveWindow);
el.classList.remove('dragging');
if (top !== undefined && left !== undefined) {
self.windowService.updatePosition(current.id, left, top);
}
}
}
updateZIndex() {
this.zIndex = Window.nextZIndex++;
this.root.el.style['z-index'] = this.zIndex;
}
}
// -----------------------------------------------------------------------------
// Two concrete Window type implementations
// -----------------------------------------------------------------------------
class HelloWorld extends Component {
static template = "HelloWorld";
static defaultTitle = "Hello Owl!";
static defaultWidth = 200;
static defaultHeight = 100;
}
class Counter extends Component {
static template = "Counter";
static defaultTitle = "Click Counter";
static defaultWidth = 300;
static defaultHeight = 120;
state = useState({ value: 0 });
inc() {
this.state.value++;
}
}
// register window components
WindowManager.Windows.Hello = HelloWorld;
WindowManager.Windows.Counter = Counter;
// -----------------------------------------------------------------------------
// Window Container
// -----------------------------------------------------------------------------
class WindowContainer extends Component {
static template = "WindowContainer";
static components = { Window };
setup() {
this.windowService = useWindowService();
}
}
// -----------------------------------------------------------------------------
// Root Component
// -----------------------------------------------------------------------------
class Root extends Component {
static template = "Root";
static components = { WindowContainer };
setup() {
this.windowService = useWindowService();
}
addWindow(type) {
this.windowService.add(type);
}
}
// -----------------------------------------------------------------------------
// Setup
// -----------------------------------------------------------------------------
const env = {
windowService: createWindowService(),
};
mount(Root, document.body, { templates: TEMPLATES, env, dev: true });
@@ -0,0 +1,32 @@
<templates>
<div t-name="Window" class="window" t-att-style="style" t-on-click="updateZIndex" t-ref="root">
<div class="header">
<span t-on-mousedown="startDragAndDrop"><t t-esc="props.info.title"/></span>
<span class="close" t-on-click.stop="close">×</span>
</div>
<t t-slot="default"/>
</div>
<div t-name="WindowContainer" class="window-manager">
<Window t-foreach="windowService.getWindows()" t-as="w" t-key="w.id" info="w">
<t t-component="w.Component"/>
</Window>
</div>
<div t-name="Root" class="app">
<WindowContainer/>
<div class="menubar">
<button t-on-click="() => this.addWindow('Hello')">Say Hello</button>
<button t-on-click="() => this.addWindow('Counter')">Counter</button>
</div>
</div>
<div t-name="HelloWorld">
Some content here...
</div>
<div t-name="Counter" class="counter">
<button t-on-click="inc">Click</button>
<span><t t-esc="state.value"/></span>
</div>
</templates>
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
import threading
import time
from http.server import SimpleHTTPRequestHandler, HTTPServer
httpd = None
def start_server():
global httpd
SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript'
httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler)
httpd.serve_forever()
url = 'http://127.0.0.1:3600'
if __name__ == "__main__":
print("Owl Application")
print("---------------")
print("Server running on: {}".format(url))
threading.Thread(target=start_server, daemon=True).start()
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
httpd.server_close()
quit(0)
+22
View File
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL App</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="app.css">
<script type="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
<script type="module" src="app.js"></script>
</head>
<body>
<pre>
This app requires a web server, simply opening the html file using your browser will not work.
You can run a simple web server with one of the following:
- if you have python3 installed, run app.py with python3 or run 'python3 -m http.server' in the app folder
- if you have node/npm installed, run 'npx serve' in the app folder
- if you have php installed, run 'php -S localhost:8080 -t .' in the app folder
</pre>
<script>if (window.location.protocol !== "file:") document.querySelector("pre").remove()</script>
</body>
</html>
@@ -1,5 +1,5 @@
<templates>
<div t-name="TabbedEditor" class="tabbed-editor">
<div t-name="TabbedEditor" class="tabbed-editor" t-att-style="props.style">
<div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown">
<t t-foreach="['js', 'xml', 'css']" t-as="tab" t-key="tab">
<a t-ref="{{tab}}" t-if="props[tab] !== false" t-key="tab" class="tab flash" t-att-class="{active: state.currentTab===tab}" t-on-click="() => setTab(tab)">
@@ -10,17 +10,17 @@
<div class="code-editor" t-ref="editor"></div>
</div>
<div t-name="App" class="playground">
<div t-name="Playground" class="playground">
<div class="left-bar" t-att-class="{split: state.splitLayout}"
t-att-style="leftPaneStyle">
<div class="menubar">
<a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a>
<select t-on-change="setSample">
<option t-foreach="SAMPLES" t-as="sample" t-key="sample_index">
<select t-on-change="onSampleChange">
<option t-foreach="samples" t-as="sample" t-key="sample_index">
<t t-esc="sample.description"/>
</option>
</select>
<a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a>
<a class="btn flash" t-on-click="exportStandaloneApp" title="Download zip that bundles this app with a tiny python web server"><i class="fas fa-download"></i></a>
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a>
<a class="btn flash share-code" t-on-click="shareCode" title="Copy a link to this playground"><span>Share</span><span class="copied-notification" t-if="state.copied">Copied link to clipboard!</span></a>
</div>
@@ -37,19 +37,19 @@
css="state.css"
xml="state.xml"
resizeable="true"
onUpdatePanelHeight="updatePanelHeight"
updatePanelHeight.bind="updatePanelHeight"
updateCode="updateCode"/>
</t>
</div>
<div class="separator vertical" t-on-mousedown="onMouseDown"/>
<div class="right-pane" t-att-style="rightPaneStyle">
<div class="right-pane">
<div class="welcome" t-if="state.displayWelcome">
<div>🦉 Odoo Web Library 🦉</div>
<div>v<t t-esc="version"/></div>
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div>
<div class="note">
<p>Note: these examples are using recent features of Javascript, and require a recent browser to work without a transpilation step!
For example, it makes use of class fields and class static fields. These examples should work in a recent Chrome version.
<p>
Note: these examples make use of recent features of Javascript, and require a recent browser to work without a transpilation step! Among other things, it uses class fields and import maps. If you encounter issues, make sure your browser is up to date.
</p>
</div>
</div>
@@ -18,12 +18,6 @@ export function debounce(func, wait, immediate) {
};
}
export function useBus(bus, evName, cb) {
bus.addEventListener(evName, cb);
owl.onWillUnmount(() => {
bus.removeEventListener(evName, cb);
});
}
const loadedScripts = {};
+5
View File
@@ -0,0 +1,5 @@
# Github page and playground
This folder contains the code for owl's [github page](https://odoo.github.io/owl)
and the owl [playground](https://odoo.github.io/owl/playground/). If you're
looking for the owl documentation, click [here](/doc)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.1.1",
"version": "2.1.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.1.1",
"version": "2.1.3",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"module": "dist/owl.es.js",
@@ -24,14 +24,13 @@
"test": "jest",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
"test:watch": "jest --watch",
"playground:serve": "python3 tools/server.py || python tools/server.py",
"playground:serve": "python3 tools/playground_server.py || python tools/playground_server.py",
"playground": "npm run build && npm run playground:serve",
"preplayground:watch": "npm run build",
"playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --write",
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --check",
"lint": "eslint src/**/*.ts tests/**/*.ts",
"publish": "npm run build && npm publish",
"release": "node tools/release.js",
"compile_templates": "node tools/compile_xml.js"
},
+1 -1
View File
@@ -134,7 +134,7 @@ class LazyValue {
* Safely outputs `value` as a block depending on the nature of `value`
*/
export function safeOutput(value: any, defaultValue?: any): ReturnType<typeof toggler> {
if (value === undefined) {
if (value === undefined || value === null) {
return defaultValue ? toggler("default", defaultValue) : toggler("undefined", text(""));
}
let safeKey;
+1 -1
View File
@@ -125,7 +125,7 @@ export class TemplateSet {
callTemplate(owner: any, subTemplate: string, ctx: any, parent: any, key: any): any {
const template = this.getTemplate(subTemplate);
return toggler(subTemplate, template.call(owner, ctx, parent, key));
return toggler(subTemplate, template.call(owner, ctx, parent, key + subTemplate));
}
}
+1 -1
View File
@@ -1,2 +1,2 @@
// do not modify manually. This file is generated by the release script.
export const version = "2.1.1";
export const version = "2.1.3";
@@ -434,6 +434,18 @@ exports[`t-out t-out with the 0 number, in a p 1`] = `
}"
`;
exports[`t-out top level t-out with null 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { safeOutput } = helpers;
return function template(ctx, node, key = \\"\\") {
return safeOutput(ctx['var']);
}
}"
`;
exports[`t-out top level t-out with undefined 1`] = `
"function anonymous(app, bdom, helpers
) {
+5
View File
@@ -62,6 +62,11 @@ describe("t-out", () => {
expect(renderToString(template, { var: undefined })).toBe("");
});
test("top level t-out with null", () => {
const template = `<t t-out="var"/>`;
expect(renderToString(template, { var: null })).toBe("");
});
test("with an extended String class", () => {
class LoveString extends String {
valueOf(): string {
@@ -42,6 +42,56 @@ exports[`t-call dynamic t-call 3`] = `
}"
`;
exports[`t-call dynamic t-call with same sub component 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const call = app.callTemplate.bind(app);
return function template(ctx, node, key = \\"\\") {
const b2 = text(ctx['current'].template);
const template1 = (ctx['current'].template);
const b3 = call(this, template1, ctx, node, key + \`__1\`);
return multi([b2, b3]);
}
}"
`;
exports[`t-call dynamic t-call with same sub component 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
return comp1({}, key + \`__1\`, node, this, null);
}
}"
`;
exports[`t-call dynamic t-call with same sub component 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`child\`);
}
}"
`;
exports[`t-call dynamic t-call with same sub component 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
return comp1({}, key + \`__1\`, node, this, null);
}
}"
`;
exports[`t-call dynamic t-call: key is propagated 1`] = `
"function anonymous(app, bdom, helpers
) {
+27
View File
@@ -398,4 +398,31 @@ describe("t-call", () => {
});
expect(fixture.innerHTML).toBe("");
});
test("dynamic t-call with same sub component", async () => {
class Child extends Component {
static template = xml`child`;
}
class Root extends Component {
static template = xml`
<t t-esc="current.template"/>
<t t-call="{{current.template}}"/>`;
static components = { Child };
current = useState({ template: "A" });
}
const root = await mount(Root, fixture, {
templates: `
<templates>
<t t-name="A"><Child/></t>
<t t-name="B"><Child/></t>
</templates>`,
});
expect(fixture.innerHTML).toBe("Achild");
root.current.template = "B";
await nextTick();
expect(fixture.innerHTML).toBe("Bchild");
});
});
+32 -14
View File
@@ -5,7 +5,7 @@ let owlStatus = 0;
const browserInstance = IS_FIREFOX ? browser : chrome;
// Used to keep track of the tabs where the owl devtools have been opened
const activePanels = new Set();
const activePanels = new Map();
// Load the devtools global hook this way when running on manifest v3 chrome
if (!IS_FIREFOX) {
@@ -75,8 +75,23 @@ browserInstance.runtime.onMessage.addListener(async (message, sender, sendRespon
return true;
} else if (message.type === "owlStatus") {
setOwlStatus(message.data);
// Dummy message to test if the extension context is still valid
// Refresh panel connection timeout
} else if (message.type === "keepAlive") {
const panel = activePanels.get(message.id);
if (panel) {
clearTimeout(panel.expirationTimeout);
panel.expirationTimeout = setTimeout(() => {
activePanels.delete(message.id);
panel.port.disconnect();
}, 750);
} else {
const port = browserInstance.runtime.connect({ name: "OwlDevtoolsPort_" + message.id });
const expirationTimeout = setTimeout(() => {
activePanels.delete(message.id);
port.disconnect();
}, 750);
activePanels.set(message.id, { port: port, expirationTimeout: expirationTimeout });
}
return;
// Open the devtools documentation in a new tab
} else if (message.type === "openDoc") {
@@ -87,10 +102,15 @@ browserInstance.runtime.onMessage.addListener(async (message, sender, sendRespon
}
);
return;
// Relay the received message to the devtools app
// Register a new port for the devtools panel
} else if (message.type === "newDevtoolsPanel") {
const tab = await getActiveTabURL();
activePanels.add(tab);
const id = message.id;
const port = browserInstance.runtime.connect({ name: "OwlDevtoolsPort_" + id });
const expirationTimeout = setTimeout(() => {
activePanels.delete(id);
port.disconnect();
}, 750);
activePanels.set(message.id, { port: port, expirationTimeout: expirationTimeout });
// This is solely for firefox which doesnt allow access to the chrome.tabs api inside devtools
} else if (message.type === "getActiveTabURL") {
getActiveTabURL().then((tab) => {
@@ -98,15 +118,13 @@ browserInstance.runtime.onMessage.addListener(async (message, sender, sendRespon
});
return true;
} else {
const tab = await getActiveTabURL();
if (!activePanels.has(tab)) {
return;
const destinationPanel = activePanels.get(sender.tab.id);
if (destinationPanel) {
destinationPanel.port.postMessage(
message.data
? { type: message.type, data: message.data, origin: message.origin }
: { type: message.type, origin: message.origin }
);
}
const port = browserInstance.runtime.connect({ name: "OwlDevtoolsPort" });
port.postMessage(
message.data
? { type: message.type, data: message.data, origin: message.origin }
: { type: message.type, origin: message.origin }
);
}
});
@@ -27,7 +27,6 @@ function createPanelsIfOwl() {
"../../assets/icon128.png",
IS_FIREFOX ? "devtools_panel.html" : "devtools_app/devtools_panel.html"
);
browserInstance.runtime.sendMessage({ type: "newDevtoolsPanel" });
}
);
}
+68 -82
View File
@@ -652,103 +652,89 @@ async function init() {
}
loadSettings();
}
// Heartbeat message to test whether the extension context is still valid or not
setInterval(() => {
if (store.extensionContextStatus) {
try {
browserInstance.runtime.sendMessage({ type: "keepAlive" });
} catch (e) {
store.extensionContextStatus = false;
browserInstance.runtime.sendMessage({ type: "newDevtoolsPanel", id: store.devtoolsId });
// Heartbeat message to test whether the extension context is still valid or not
setInterval(() => {
if (store.extensionContextStatus) {
try {
browserInstance.runtime.sendMessage({ type: "keepAlive", id: store.devtoolsId });
} catch (e) {
store.extensionContextStatus = false;
}
}
}
}, 1000);
}, 500);
}
let flushRendersTimeout = false;
// Connect to the port to communicate to the background script
browserInstance.runtime.onConnect.addListener((port) => {
if (!port.name === "OwlDevtoolsPort") {
return;
}
port.onMessage.addListener(async (msg) => {
// Reload the tree after checking if the scripts are loaded when this message is received
if (msg.type === "Reload") {
const tab = await getTabURL();
// Since this message is sent to all devtools windows, only take it into account when this is the active tab
if (tab !== store.devtoolsId) {
return;
if (port.name === "OwlDevtoolsPort_" + store.devtoolsId) {
port.onMessage.addListener(async (msg) => {
// Reload the tree after checking if the scripts are loaded when this message is received
if (msg.type === "Reload") {
store.owlStatus = await evalInWindow("window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;");
if (store.owlStatus) {
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
store.resetData();
}
}
store.owlStatus = await evalInWindow("window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;");
if (store.owlStatus) {
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
// Received when a frame has been delayed when loading the scripts due to owl being lazy loaded
if (msg.type === "FrameReady") {
store.updateIFrameList();
store.owlStatus = true;
store.resetData();
}
}
// Received when a frame has been delayed when loading the scripts due to owl being lazy loaded
if (msg.type === "FrameReady") {
const tab = await getTabURL();
// Same as for the reload message
if (tab !== store.devtoolsId) {
return;
// We need to reload the components tree when the set of apps in the page is modified
if (msg.type === "RefreshApps") {
store.loadComponentsTree(true);
}
store.updateIFrameList();
store.owlStatus = true;
store.resetData();
}
// We need to reload the components tree when the set of apps in the page is modified
if (msg.type === "RefreshApps") {
store.loadComponentsTree(true);
}
// Filter out the messages that are not destined to this devtools tab. The messages above may be sent before
// the devtoolsId is set
if (msg.origin.id !== store.devtoolsId) {
return;
}
// When message of type Flush is received, overwrite the component tree with the new one from page
// A flush message is sent everytime a component is rendered on the page
if (msg.type === "Flush") {
if (msg.origin.frame !== store.activeFrame) {
return;
// When message of type Flush is received, overwrite the component tree with the new one from page
// A flush message is sent everytime a component is rendered on the page
if (msg.type === "Flush") {
if (msg.origin.frame !== store.activeFrame) {
return;
}
if (!(Array.isArray(msg.data) && msg.data.every((val) => typeof val === "string"))) {
return;
}
// This determines which components will have a short highlight effect in the tree to indicate they have been rendered
store.renderPaths.add(JSON.stringify(msg.data));
clearTimeout(flushRendersTimeout);
flushRendersTimeout = setTimeout(() => {
store.renderPaths.clear();
}, 100);
store.loadComponentsTree(true);
}
if (!(Array.isArray(msg.data) && msg.data.every((val) => typeof val === "string"))) {
return;
// Select the component based on the path received with the SelectElement message
if (msg.type === "SelectElement") {
if (!(Array.isArray(msg.data) && msg.data.every((val) => typeof val === "string"))) {
return;
}
store.selectComponent(msg.data);
}
// This determines which components will have a short highlight effect in the tree to indicate they have been rendered
store.renderPaths.add(JSON.stringify(msg.data));
clearTimeout(flushRendersTimeout);
flushRendersTimeout = setTimeout(() => {
store.renderPaths.clear();
}, 100);
store.loadComponentsTree(true);
}
// Select the component based on the path received with the SelectElement message
if (msg.type === "SelectElement") {
if (!(Array.isArray(msg.data) && msg.data.every((val) => typeof val === "string"))) {
return;
// Stop the DOM element selector tool upon receiving the StopSelector message
if (msg.type === "StopSelector") {
store.componentSearch.activeSelector = false;
}
store.selectComponent(msg.data);
}
// Stop the DOM element selector tool upon receiving the StopSelector message
if (msg.type === "StopSelector") {
store.componentSearch.activeSelector = false;
}
// Logic for recording an event when the event message is received
if (msg.type === "Event") {
let events = msg.data;
loadEvents(events);
}
// If we know a new iframe has been added to the page, load scripts into it and update the
// frames list if it has been directly loaded.
if (msg.type === "NewIFrame") {
const isLoaded = await loadScripts(msg.data);
if (isLoaded) {
store.updateIFrameList();
// Logic for recording an event when the event message is received
if (msg.type === "Event") {
let events = msg.data;
loadEvents(events);
}
}
});
// If we know a new iframe has been added to the page, load scripts into it and update the
// frames list if it has been directly loaded.
if (msg.type === "NewIFrame") {
const isLoaded = await loadScripts(msg.data);
if (isLoaded) {
store.updateIFrameList();
}
}
});
}
});
// Load all settings from the chrome sync storage
@@ -17,8 +17,6 @@
this.addedElements = [];
// To keep track of the succession order of the render events
this.eventId = 0;
// Will be reset as soon as a new devtools owl tab is opened. Allows to avoid sending messages to the wrong devtools tab later on
this.devtoolsId = 0;
// Set to keep track of the frame on which this script is loaded
this.frame = "top";
// Allows to launch a message each time an iframe html element is added to the page
@@ -38,7 +36,6 @@
source: "owl-devtools",
type: "NewIFrame",
data: addedNode.contentDocument.location.href,
origin: { id: self.devtoolsId },
});
}
}
@@ -47,10 +44,8 @@
});
iFrameObserver.observe(document.body, { subtree: true, childList: true });
this.appsPatched = false;
this.destroyPatched = false;
this.patchAppsSetMethods();
setTimeout(() => {
this.patchAppMethods();
}, 200);
this.recordEvents = false;
this.traceRenderings = false;
this.traceSubscriptions = false;
@@ -177,6 +172,26 @@
const self = this;
this.apps.add = function () {
originalAdd.call(this, ...arguments);
if (!self.destroyPatched) {
const newApp = arguments[0];
// It is not a given that apps have a root node at creation so we need to wait
if (newApp.root) {
self.patchDestroyMethod(newApp.root);
} else {
let root = null;
Object.defineProperty(newApp, "root", {
get() {
return root;
},
set(value) {
root = value;
if (!self.destroyPatched) {
self.patchDestroyMethod(root);
}
},
});
}
}
self.patchAppMethods();
window.top.postMessage({
source: "owl-devtools",
@@ -192,18 +207,40 @@
};
}
patchDestroyMethod(root) {
if (!this.destroyPatched) {
// Signals when a component is destroyed
const originalDestroy = root.constructor.prototype._destroy;
const self = this;
root.constructor.prototype._destroy = function () {
if (self.recordEvents) {
const path = self.getComponentPath(this);
const event = {
type: "destroy",
component: this.name,
key: this.parentKey,
path: path,
time: 0,
id: self.eventId++,
};
self.eventsBatch.push(event);
const before = performance.now();
originalDestroy.call(this, ...arguments);
event.time = performance.now() - before;
} else {
originalDestroy.call(this, ...arguments);
}
};
this.destroyPatched = true;
}
}
// Modify methods of each app so that it triggers messages on each flush and component render
patchAppMethods() {
if (this.appsPatched) {
return;
}
let app;
for (const appItem of this.apps) {
if (appItem.root) {
app = appItem;
}
}
// We don't want to bother patching the apps methods if none have components inside
let app = this.apps.values().next().value;
if (!app) {
return;
}
@@ -223,7 +260,7 @@
source: "owl-devtools",
type: "Flush",
data: path,
origin: { id: self.devtoolsId, frame: self.frame },
origin: { frame: self.frame },
});
}
});
@@ -332,32 +369,10 @@
source: "owl-devtools",
type: "Event",
data: self.eventsBatch,
origin: { id: self.devtoolsId },
});
self.eventsBatch = [];
}
};
// Signals when a component is destroyed
const originalDestroy = app.root.constructor.prototype._destroy;
app.root.constructor.prototype._destroy = function () {
if (self.recordEvents) {
const path = self.getComponentPath(this);
const event = {
type: "destroy",
component: this.name,
key: this.parentKey,
path: path,
time: 0,
id: self.eventId++,
};
self.eventsBatch.push(event);
const before = performance.now();
originalDestroy.call(this, ...arguments);
event.time = performance.now() - before;
} else {
originalDestroy.call(this, ...arguments);
}
};
this.appsPatched = true;
}
@@ -609,7 +624,6 @@
source: "owl-devtools",
type: "SelectElement",
data: path,
origin: { id: this.devtoolsId },
});
}
};
@@ -637,7 +651,6 @@
window.top.postMessage({
source: "owl-devtools",
type: "StopSelector",
origin: { id: this.devtoolsId },
});
};
@@ -1748,6 +1761,6 @@
false
);
checkOwlStatus();
// Indicates whether the scripts loaded successfully or not (useful when loaded with eval in iframes only)
// Indicates whether the scripts loaded successfully or not (only useful when loaded with eval in iframes)
return window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;
})();
-123
View File
@@ -1,123 +0,0 @@
import { loadJS } from "./utils.js";
const sourceCache = new Map();
async function getSourceCode(filePath) {
if (sourceCache.has(filePath)) {
return sourceCache.get(filePath);
}
let source = await fetch(`${filePath}.js`);
source = await source.text();
sourceCache.set(filePath, source);
return source;
}
const DEFAULT_HTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL App</title>
<link rel="icon" href="data:,">
<script src="owl.js"></script>
<link rel="stylesheet" href="app.css">
<script type="module" src="app.js"></script>
<script type="module" src="utils.js"></script>
</head>
<body>
</body>
</html>
`;
const APP_PY = `#!/usr/bin/env python3
import threading
import time
from http.server import SimpleHTTPRequestHandler, HTTPServer
def start_server():
SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript'
httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler)
httpd.serve_forever()
url = 'http://127.0.0.1:3600'
if __name__ == "__main__":
print("Owl Application")
print("---------------")
print("Server running on: {}".format(url))
threading.Thread(target=start_server, daemon=True).start()
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
httpd.server_close()
quit(0)
`;
/**
* Make a zip file containing a functioning application
*/
export async function exportStandaloneApp(js, css, xml) {
await loadJS("libs/jszip.min.js");
const zip = new JSZip();
const processedJS = js
.split("\n")
.map(l => (l === "" ? "" : " " + l))
.join("\n");
const JS = `
/**
* This is the javascript code defined in the playground.
* In a larger application, this code should probably be moved in different
* sub files.
*/
import * as utils from "./utils.js";
async function app() {
${processedJS}
}
/**
* Initialization code
* This code load templates, and make sure everything is properly connected.
*/
function prepareOWLApp(templates) {
const _configure = owl.App.prototype.configure;
owl.App.prototype.configure = function configureOverriden(config) {
config = Object.assign({ dev: true }, config);
this.addTemplates(templates);
return _configure.call(this, config);
}
}
async function start() {
let templates;
try {
templates = await owl.loadFile('app.xml');
} catch(e) {
console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
return;
}
prepareOWLApp(templates);
app();
}
start();
`;
const [owlSrc, utilsSrc] = await Promise.all([
getSourceCode("../owl"),
getSourceCode("./utils"),
]);
zip.file("app.js", JS);
zip.file("utils.js", utilsSrc);
zip.file("app.css", css);
zip.file("app.py", APP_PY);
zip.file("app.xml", xml);
zip.file("index.html", DEFAULT_HTML);
zip.file("owl.js", owlSrc);
return zip.generateAsync({ type: "blob" });
}
-22
View File
@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OWL Playground</title>
<link rel="icon" href="data:,">
<script src="libs/ace.js" type="text/javascript" charset="utf-8"></script>
<script src="../owl.js"></script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/solid.css" integrity="sha384-QokYePQSOwpBDuhlHOsX0ymF6R/vLk/UQVz3WHa6wygxI5oGTmDTv8wahFOSspdm" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/fontawesome.css" integrity="sha384-vd1e11sR28tEK9YANUtpIOdjGW14pS87bUBuOIoBILVWLFnS+MCX9T6MMf0VdPGq" crossorigin="anonymous">
<!-- Application JS/CSS -->
<link rel="stylesheet" href="playground.css">
<script type="module" src="export_snippet.js"></script>
<script type="module" src="utils.js"></script>
<script type="module" src="app.js"></script>
</head>
<body>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -6,7 +6,7 @@ from http.server import SimpleHTTPRequestHandler, HTTPServer
HOST = '127.0.0.1'
PORT = 8000
URL = 'http://{0}:{1}/tools/playground'.format(HOST, PORT)
URL = 'http://{0}:{1}/docs/playground'.format(HOST, PORT)
# We define our own handler here to remap owl.js GET requests to the Owl build
@@ -15,8 +15,8 @@ URL = 'http://{0}:{1}/tools/playground'.format(HOST, PORT)
# we want to update the playground.
class OWLHandler(SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == '/tools/owl.js':
self.path = '/dist/owl.iife.js'
if self.path == '/docs/owl.js' or self.path == '/docs/playground/owl.js':
self.path = '/dist/owl.es.js'
return SimpleHTTPRequestHandler.do_GET(self)
def end_headers(self):
+40 -69
View File
@@ -7,7 +7,6 @@ const chalk = require("chalk");
const branchName = require('current-git-branch');
const REL_NOTES_FILE = `release-notes.md`;
const STEPS = 8;
const branch = "master";
const rl = readline.createInterface({
@@ -29,26 +28,28 @@ async function startRelease() {
logError(`You shall not pass! You are not on the ${branch} branch!`)
return;
}
log(`*** Owl release script ***`);
log(`Current Version: ${package.version}`);
log("Check if code formatting is right...")
const STEPS = 11;
let step = 1;
// ---------------------------------------------------------------------------
log(`Step ${step++}/${STEPS}: Checking if code formatting is right...`)
const checkFormatting = await execCommand("npm run check-formatting");
if (checkFormatting !== 0) {
logError("Prettier format validation failed. Aborting.");
return;
}
log(`*** Owl release script ***`);
log(`Current Version: ${package.version}`);
// ---------------------------------------------------------------------------
log(`Step 1/${STEPS}: collecting info...`);
log(`Step ${step++}/${STEPS}: collecting info...`);
let next = await ask("Next version: ");
if (next[0] === 'v') next = next.substring(1);
let file = await ask(`Release notes (${REL_NOTES_FILE}): `);
file = file || REL_NOTES_FILE;
let content;
if (!fs.existsSync(`./${file}`)) {
let lastRelease = await getOutput("git log --grep='\\[REL\\]' -n 1 --pretty=%H");
const lastRelease = await getOutput("git log --grep='\\[REL\\]' -n 1 --pretty=%H");
const commitsSinceLastRelease = await getOutput(`git log ${lastRelease.trim()}..HEAD --pretty=%s`);
const commitsAsMdList = commitsSinceLastRelease.trim().split("\n").map(l => " - " + l).join("\n");
log(`${file} did not exist, created a template containing all commits since last release.`)
@@ -59,6 +60,7 @@ async function startRelease() {
return;
}
}
let content;
try {
content = await readFile("./" + file);
} catch (e) {
@@ -68,15 +70,12 @@ async function startRelease() {
}
let shouldBeDraft = await ask(`Should be a draft [y/n] ? (n)`);
let draft = ""
if (shouldBeDraft.toLowerCase() === 'y')
{
if (shouldBeDraft.toLowerCase() === 'y') {
draft = "--draft";
}
let shouldUploadPlayground = await ask(`Should this release be uploaded on the playground [y/n] ? (y)`);
shouldUploadPlayground = shouldUploadPlayground.toLowerCase() !== 'n';
// ---------------------------------------------------------------------------
log(`Step 2/${STEPS}: running tests...`);
log(`Step ${step++}/${STEPS}: running tests...`);
const testsResult = await execCommand("npm run test");
if (testsResult !== 0) {
logError("Test suite does not pass. Aborting.");
@@ -84,28 +83,16 @@ async function startRelease() {
}
// ---------------------------------------------------------------------------
log(`Step 3/${STEPS}: updating package.json...`);
await writeFile("package.json", JSON.stringify({...package, version: next}, null, 2) + "\n");
await writeFile("package-lock.json", JSON.stringify({...packageLock, version: next}, null, 2) + "\n");
await writeFile("./src/version.ts", `// do not modify manually. This file is generated by the release script.\nexport const version = "${next}";\n`);
// ---------------------------------------------------------------------------
log(`Step 4/${STEPS}: creating git commit...`);
const escapedContent = content.replace(/\"/g, '\\\"').replace(/\`/g, '\\\`');
const gitResult = await execCommand(`git commit -am "[REL] v${next}\n\n${escapedContent}"`);
if (gitResult !== 0) {
logError("Git commit failed. Aborting.");
return;
}
// ----------------------------------------------------------------------------
log(`Step 5/${STEPS}: building owl and devtools...`);
log(`Step ${step++}/${STEPS}: building owl...`);
await execCommand("rm -rf dist/");
const buildResult = await execCommand("npm run build");
if (buildResult !== 0) {
logError("Build failed. Aborting.");
return;
}
// ----------------------------------------------------------------------------
log(`Step ${step++}/${STEPS}: building devtools...`);
const chromeResult = await execCommand("npm run build:devtools-chrome");
if (chromeResult !== 0) {
logError("Build devtools chrome failed. Aborting.");
@@ -122,7 +109,27 @@ async function startRelease() {
await execCommand("rm -r dist/devtools-chrome dist/devtools-firefox && rm dist/compiler.js");
// ---------------------------------------------------------------------------
log(`Step 6/${STEPS}: pushing on github...`);
log(`Step ${step++}/${STEPS}: updating package.json...`);
await writeFile("package.json", JSON.stringify({...package, version: next}, null, 2) + "\n");
await writeFile("package-lock.json", JSON.stringify({...packageLock, version: next}, null, 2) + "\n");
await writeFile("./src/version.ts", `// do not modify manually. This file is generated by the release script.\nexport const version = "${next}";\n`);
// ---------------------------------------------------------------------------
log(`Step ${step++}/${STEPS}: updating owl on github page...`);
await fs.copyFileSync("dist/owl.es.js", "docs/owl.js");
// ---------------------------------------------------------------------------
log(`Step ${step++}/${STEPS}: creating git commit...`);
const escapedContent = content.replace(/\"/g, '\\\"').replace(/\`/g, '\\\`');
const gitResult = await execCommand(`git commit -am "[REL] v${next}\n\n${escapedContent}"`);
if (gitResult !== 0) {
logError("Git commit failed. Aborting.");
return;
}
// ---------------------------------------------------------------------------
log(`Step ${step++}/${STEPS}: pushing on github...`);
const pushResult = await execCommand("git push origin " + branch);
if (pushResult !== 0) {
logError("git push failed. Aborting.");
@@ -131,55 +138,19 @@ async function startRelease() {
// ---------------------------------------------------------------------------
log(`Step 7/${STEPS}: Creating the release...`);
log(`Step ${step++}/${STEPS}: Creating the release...`);
const relaseResult = await execCommand(`gh release create v${next} dist/*.js dist/*.zip ${draft} -F ${file}`);
if (relaseResult !== 0) {
logError("github release failed. Aborting.");
return;
}
log(`Step 8/${STEPS}: publishing module on npm...`);
await execCommand("npm run publish");
log(`Step ${step++}/${STEPS}: publishing module on npm...`);
await execCommand("npm publish");
log("Owl Release process completed! Thank you for your patience");
await execCommand(`gh release view`);
await execCommand(`gh release view -w`);
if (shouldUploadPlayground) {
log(`Bonus step: publishing new release on playground...`);
let owl_code = null;
let status = 0
try {
owl_code = await readFile("dist/owl.iife.js");
} catch (e) {
logSubContent(e.message);
logError("Cannot read owl.iife.js... Aborting");
return;
}
status |= await execCommand("git checkout gh-pages");
status |= await execCommand("git pull --rebase");
if (status !== 0) {
logError("Couldn't switch to gh-pages branch")
return;
}
try {
fs.writeFileSync('owl.js', owl_code)
} catch (err) {
logError(err)
return;
}
status |= await execCommand(`git commit -am "[IMP] update owl to v${next}"`);
status |= await execCommand(`git push origin gh-pages`);
status |= await execCommand("git checkout -");
if (status !== 0) {
logError("Something went wrong for the playground update.")
}
}
}
// -----------------------------------------------------------------------------