Initial commit

Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com>
Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com>
Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com>
Co-Authored-By: Michael Cartner <32543275+michaelcartner@users.noreply.github.com>
This commit is contained in:
Eric Tuvesson
2023-09-05 12:08:55 +02:00
commit 53f0d6320e
2704 changed files with 76354 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
build/*
node_modules/*
.docusaurus
static/docs/guides/user-interfaces/layout/.DS_Store
static/docs/guides/user-interfaces/.DS_Store
static/docs/guides/.DS_Store
.DS_Store

132
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,132 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of
any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address,
without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[INSERT CONTACT METHOD].
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

674
LICENSE.md Normal file
View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

53
README.md Normal file
View File

@@ -0,0 +1,53 @@
# Noodl docs
Welcome to the Noodl docs project! This is a open-source project aimed at providing detailed and up-to-date documentation for users of Noodl. Whether you're a beginner, intermediate, or advanced Noodl user, you will find this documentation invaluable.
## About Noodl
Noodl is the low-code platform where designers and developers build custom applications and experiences. Designed as a visual programming environment, it aims to expedite your development process. It promotes swift and efficient creation of applications, requiring minimal coding knowledge.
## Getting Started
This project is using [Docusaurus](https://docusaurus.io/) to help us ship a beautiful documentation site.
In order to run this project locally, you need to follow the steps below:
```bash
# Clone the repository to your local machine using the command below in your terminal or an external git client.
git clone https://github.com/noodlapp/noodl-docs.git
# Navigate into the project's directory.
cd repository
# Install the project dependencies.
npm install
# Start the development server.
npm run start
```
Now, you should be able to view the project in your preferred browser through `localhost:3000`.
## FAQ
##### Can I preview my changes in the Noodl editor?
Yes, when the Noodl editor starts up it will check anything running on `localhost:3000` and if it recognize that it is the Noodl docs it will use the local endpoint instead of the live docs.
##### How do we deploy the changes?
Currently people at Noodl will deploy the changes manually, until we have an automatic system in place.
## Contributing
We welcome contributions from the community. Whether you've found a typo, incorrect information, or you want to make large-scale updates or additions, we appreciate your effort.
## License
Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
This project is licensed under the GPL License - see the [LICENSE.md](LICENSE.md) file for details.
## Contact
If you have any questions, concerns, or feedback, please open a discussion in the [discussions tracker](https://github.com/noodlapp/noodl-docs/discussions) or join our Discord channel and we'll be happy to assist you!

3
babel.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};

View File

@@ -0,0 +1,30 @@
---
title: CLI Command build
hide_title: true
---
# Command: build
Build a Noodl app without having Noodl installed and with custom build settings.
## Synopsis
```
noodl-cli build <projectDir> <outputDir> [--parseEndpoint <endpoint>] [--parseAppId <appId>]
```
### Configuration
#### `parseEndpoint`
- Default: undefined
- Type: String
The endpoint to the Parse Platform instance.
#### `parseAppId`
- Default: undefined
- Type: String
The App ID for the Parse Platform instance.

View File

@@ -0,0 +1,41 @@
---
title: CLI Command new
hide_title: true
---
# Command: new
Create a new Noodl module template.
### Example
```bash
# Create a React Module in the relative "./my-module" folder.
noodl-cli new react-lib ./my-module
```
### Available Templates
#### `react-lib`
Create a module with React and Logic Component support.
#### `lib`
Create a module with Logic Component support.
## Synopsis
```
noodl-cli new <template> <path> [--name <name>]
```
### Configuration
#### `name`
- Default: folder name
- Type: String
- Alias: `-n`
The module name.

View File

@@ -0,0 +1,16 @@
---
title: CLI Command parse
hide_title: true
---
# Command: parse
Interact with a Parse Platform instance.
I would recommend looking at [noodl-parse-schema-sync (Github)](https://github.com/noodlapp/noodl-parse-schema-sync) since that is more up to date.
## Synopsis
```
noodl-cli parse sync-schema
```

View File

@@ -0,0 +1,43 @@
---
title: CLI Command project list
hide_title: true
---
# Command: project list
Clone a Noodl project via git.
:::tip
For this to work it is required that git is install on the machine.
:::
## Synopsis
```
noodl-cli project clone <project-id> <path> [--username <username>] [--password <password>] [--shell <shell>]
```
### Configuration
#### `username`
- Default: undefined
- Type: String
Noodl username.
#### `password`
- Default: undefined
- Type: String
Noodl password.
#### `shell`
- Default: undefined
- Type: String
Pass a different shell to git clone.

View File

@@ -0,0 +1,30 @@
---
title: CLI Command project list
hide_title: true
---
# Command: project list
List all the projects in a workspace.
## Synopsis
```
noodl-cli project list <workspace-id> [--username <username>] [--password <password>]
```
### Configuration
#### `username`
- Default: undefined
- Type: String
Noodl username.
#### `password`
- Default: undefined
- Type: String
Noodl password.

View File

@@ -0,0 +1,30 @@
---
title: CLI Command workspace
hide_title: true
---
# Command: workspace list
List all the workspaces.
## Synopsis
```
noodl-cli workspace list [--username <username>] [--password <password>]
```
### Configuration
#### `username`
- Default: undefined
- Type: String
Noodl username.
#### `password`
- Default: undefined
- Type: String
Noodl password.

View File

@@ -0,0 +1,33 @@
---
title: Continuous Deployment
hide_title: true
---
# Continuous Deployment
The Noodl CLI have commands that can be used to interact with your Noodl projects.
:::note
These commands require authentication.
:::
## Clone Noodl Project
The project clone command `noodl-cli project clone` will call `git clone`, expecting that `git` is already installed on the machine.
```bash
$ noodl-cli project clone \
--workspace-id <workspace-id> \
--project-id <project-id> \
--path <path>
```
## Build Noodl Project
```bash
$ noodl-cli build \
--projectDir <project-path> \
--outFile <path/file.zip>
```

View File

@@ -0,0 +1,24 @@
---
title: Workspace Modules
hide_title: true
---
# Workspace Modules
:::danger
Currently, the new Noodl CLI doesn't have support to upload to a workspace, it is supported in the older version of the Noodl CLI, for example, version `0.7.2`.
:::
## Using the old Noodl CLI
To find the workspace id and access key.
Open the [Noodl console](https://console.noodl.net/) and manage a workspace, if you are admin you will have access to the **Access Key**. The workspace id can be found in the URL on that page, for example `https://console.noodl.net/#/workspaces/g58p2h` have the **workspace id** `g58p2h`.
Run this in the same folder where you have `module.json`.
```bash
$ noodl-cli push <workspace-id>/<access-key>
```

44
cli/overview.mdx Normal file
View File

@@ -0,0 +1,44 @@
---
title: CLI Overview
hide_title: true
---
# CLI Overview
Noodl CLI, or `@noodl/noodl-cli`, is a command-line interface to Noodl for use in your terminal or your scripts.
## Installation
```bash
$ npm install -g @noodl/noodl-cli
```
We have a few guides to help you get started with the Noodl CLI.
To get more information about the Noodl CLI, you can call `noodl-cli help` to see the possible commands.
## Authentication
Some commands in `noodl-cli` require you to authenticate with Noodl.
Set the credentials via environment variables:
```bash
$ export NOODL_USERNAME=<my-email>
$ export NOODL_PASSWORD=<my-password>
```
You can also pass in the email and password via the command that requires it.
```
$ noodl-cli project clone <project-id> <path> --username <my-email> --password <my-password>
```
## Retrieving workspace id or project id
```bash
$ noodl-cli workspace list --username <my-email> --password <my-password>
```
```bash
$ noodl-cli project list <workspace-id> --username <my-email> --password <my-password>
```

View File

@@ -0,0 +1,10 @@
---
hide_title: true
title: Horizontal List with Snapping
---
# Horizontal List with Snapping
This short build along shows how to create a Horizontal list with Cards. It will show you how to work with advanced **Scrolling** in lists.
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/x0nWHv2uCHI" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe>

View File

@@ -0,0 +1,11 @@
---
hide_title: true
title: Build Alongs
---
# Build Alongs
One relaxed way of learning Noodl is to watch and learn from the experts. Here are a list of 'build-along' type of movies that shows in detail how to build something in Noodl. Enjoy!

View File

@@ -0,0 +1,12 @@
---
hide_title: true
title: Star Rating Component
---
# Star Rating Component
This short build along shows how to work with _animations_, _visual states_ and _component logic_ to build a visually rich star rating component.
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/VwgcIsclVpE" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe>
You can import the full project [here](/library/examples/star-rating-component).

View File

@@ -0,0 +1,22 @@
---
hide_title: true
title: Custom Survey App Build Along
---
# Custom Survey App Build Along
In this three parts video series, Johan walks your through the creation of a simple Survey App. This build along mainly covers **Navigation**, **styling of components** and how to create more advanced **custom components**.
You can import the full project [here](/library/examples/survey-app).
## Part 1 - Navigation & Components
<iframe width="560" height="315" src="https://www.youtube.com/embed/ntKTCC7eEk0" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe>
## Part 2 - Styling Interaction States
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/720U1pZqWJg" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe>
## Part 3 - Advanced Custom Components
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/ZiEddnfPrx8" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe>

View File

@@ -0,0 +1,18 @@
---
hide_title: true
title: Task List App Build Along
---
# Task List App Build Along
In this two parts video series, Johan will walk you through how he builds a task list app. This Build Along is excellent for beginners since it will take you through all of the basic functionality of Noodl.
You can import the full project [here](/library/examples/task-list-app).
## Part 1 - UI Controls and Layout
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/TNnn0Gzj-H4" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe>
## Part 2 - Connecting to the Noodl Backend
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/HCqlSUyguiE" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe>

View File

@@ -0,0 +1,162 @@
---
title: Develop with ChatGPT
hide_title: true
---
# Develop with ChatGPT
ChatGPT is a great tool that is used daily to generate code by developers all over the world. However (being a language model and not a programming engine) it does come with its own set of quirks and limitations. In this guide we will look at tips, tricks and strategies for how to get around them, and see why ChatGPT is a great pairing with Noodl.
> This guide is about how to use [http://chat.openai.com](http://chat.openai.com) as part of the development process.<br/><br/>To use OpenAI as part of your application, check out the [OpenAI prefab](/library/prefabs/openai/).
## When to ChatGPT
One of the lesser known limitations of ChatGPT is that it works with a tiny token memory. The details are technical, and not too interesting in the context of this guide, so we won't go in on it too much. Really, the only thing you need to know is that ChatGPT has the memory of a goldfish, and will start forgetting things (including its own train of thought) pretty quickly if you feed it too much information.
This makes ChatGPT bad at handling large scale application code with many different moving parts. Using ChatGPT to code an application from scratch requires you to know how to program an application from scratch so that you can make up for all the mistakes the AI makes.
Another implication of the tiny memory is that it limits the amount of instructions we can provide. ChatGPT gives the best solutions when you are very descriptive with what you want to achieve, and a full application requires a lot of describing.
The best way to get good code from ChatGPT is really to break up your application into smaller modular pieces, generating those with AI, and gluing them together yourself.
This is where Noodl shines. Since one of the base concepts of Noodl is to break your application into smaller reusable components, we dont have to ask ChatGPT to generate a whole app for us. We can just ask it to do the boring, tedious or more complicated functions. This still requires a little bit of programming know-how, but thanks to Noodls seamless combination of visual and text-based programming you don't have to know how to write complex apps at scale and can instead focus on understanding the functionality at hand.
:::note
While this guide is generally about ChatGPT you will get a lot more out of it if you already have some experience with Noodl. It might be a good idea to check out the [Fundamentals](/docs/getting-started/fundamentals) guide, or do the interactive tutorials in the Noodl editor first.
:::
### TL;DR:
ChatGPT is great at generating code for Function and Script nodes, helping you connect to API's, work with external libraries, aggregating data or figuring out chunks of your app.
It's a good idea to modularize your functions into separate Function nodes, or combine multiple Function nodes in series in a component. This allows ChatGPT to shine in a smaller scope while allowing for great reusability within Noodl.
ChatGPT is also great as a search engine that you can have a conversation with. The ChatGPT model is trained on data up until 2021, so while it's not good for researching current events, it is very powerful with more general development patterns that can easily be translated into Noodl graphs.
Sidenote: We have been experimenting with generating full Noodl Node graphs, but due to the limited memory it's not too good at memorizing current documentation.
## Priming for code generation
If we want to use ChatGPT for code generation we have to be prepared to modify the code it gives us. That is just the current state of the AI. However, we can minimize the need for modifications if we start the chat with some instructions, giving ChatGPT a bit of context so that it understands what a Noodl function looks like, some rules for what it can/can't do and how to create inputs and outputs to the node. This is called a _primer_, and here is one that we have been experimenting with:
````markdown
Hi ChatGPT. Here are your instructions. You must follow all of them.
- You will be writing Noodl functions.
- An input in a Noodl function must follow the format "Inputs.InputName".
- An input in a Noodl function is only read, never written to.
- An output in a Noodl function must follow the format "Outputs.OutputName = value".
- A variable in a Noodl function never stores an output.
- Sending a signal from a Noodl function must follow the format "Outputs.SignalName()".
- Signals can not be passed values. All output values must be set as a Noodl function output.
- Inputs and Outputs in a Noodl function are global.
- Noodl functions do not use import statements.
- Noodl functions do not use export statements.
- Noodl functions can use recources from a CDN.
- Noodl functions can access API endpoints with "fetch".
- Define constants as Noodl function inputs.
- A Noodl function follows this format:
```js
const inputName = Inputs.InputName;
// Check if the input has a value, otherwise return
if (!inputName) return;
// Perform the function logic
```
Reply "Okidoki" if the instructions are clear, otherwise ask me to clarify
````
This primer has given us great results so far - in many cases the snippets have worked with no modifications at all. Don't shy away from modifying it or trying a different approach though. ChatGPT is a complex technology, and new techniques and strategies are constantly being discovered. If you find something interesting, please don't shy away from sharing it with the community over at our [Discord server](https://discord.com/invite/23xU2hYrSJ).
## Tips and tricks for prompting code
### Benefits of a primer
Using the primer above lets us use very sloppy prompts, while still getting a useful result:
![](/docs/getting-started/ai-assisted-dev/chat-gpt/sloppy-prompt.png)
Due to the primer, this result can be copied stright into a Function node without any modifications. Here is how it looks in a node graph with all the inputs and outputs:
<div class="ndl-image-with-background xl">
![](/docs/getting-started/ai-assisted-dev/chat-gpt/sloppy-prompt-connected.png)
</div>
All we needed to provide was an API key. Amazing!
### Adding detail
The example above is very simple. The more complex functionality we want, the more specific we need to be. ChatGPT has a lot of imagination, and it's a good idea to leave it as little room for interpretation as possible.
![](/docs/getting-started/ai-assisted-dev/chat-gpt/detailed-prompt.png)
This was my third attempt. In the first one I asked it to _prompt the user for their location_. It then gave me a soultion using the `Window.prompt()` method. This opens up a system dialog with a text input where the user can type in the `latitude` and `longitude` themselves. It bears noting that the last time `Window.prompt()` was used in a serious project, the smartphone was still a new and revolutionary invention.
In the second attempt I asked it to _get the users location_. Then it just assumed that the `latitude` and `longitude` was a part of the `user` object. Not specific enough.
The third attempt looks perfect, at least at a first glance. The users location is now gotten automatically.
The location was not the only issue though. If we take a closer look at the code we see that there are some places where ChatGPT didn't follow the prompt. We asked it to send an `Outputs.UserFailed()` if `user.firstName` or `user.lastName` where missing. Instead it sends the `UserFailed` signal if it cant find a user of the right ID. It also sends `MapboxData` and `FullName` separately. It _almost_ does what we told it. Discrepancies like this can happen anywhere in the generated code, so it's always a good idea to look over the code you get.
These off-prompt moments are not only a bad thing though. As an example, we didn't ask ChatGPT to exit the function using `return` when after sending the `UserFailed` signal, but it was clever enough to assume that we didn't want the function to continue running without a user.
### Partial rewrites
If we find that parts of the code doesn't fit us, we can always ask ChatGPT to iterate on it.
![](/docs/getting-started/ai-assisted-dev/chat-gpt/iterate-prompt.png)
It now exits the function when it should, and outputs the data properly. There are still unnecessary checks made for `firstName` and `lastName` when building the `fullName`. They will not throw any errors, or mess up the function in any other way, and the performance hit is so small that it can be ignored, but if we want to keep the code clean we can just manually remove those two lines.
### Iterative prompting
If we want even more control we can prompt our function in smaller steps. Let's generate a function that does a device generalisation based on the viewport width.
![](/docs/getting-started/ai-assisted-dev/chat-gpt/chain-prompt-1.png)
![](/docs/getting-started/ai-assisted-dev/chat-gpt/chain-prompt-2.png)
![](/docs/getting-started/ai-assisted-dev/chat-gpt/chain-prompt-3.png)
Note that I asked for `widths` but it gave me heights as well! Let's clear that up.
![](/docs/getting-started/ai-assisted-dev/chat-gpt/chain-prompt-4.png)
![](/docs/getting-started/ai-assisted-dev/chat-gpt/chain-prompt-5.png)
Here I realize that I made two mistakes. First of all, I was hoping for ChatGPT to be a bit clever and assume that I meant to check for a value inside of the desktop range. Instead it tries to match an exact width, which will fail if the browser is resized. The second error was to output a `IsDesktop` boolean. If we check for ranges with the given sizes, there will be a gap between desktop and mobile. Let's make a lazy fix.
![](/docs/getting-started/ai-assisted-dev/chat-gpt/chain-prompt-6.png)
Now, this is not a perfect function by far. Good thing is that this prompting could go on and on, until we have a function that gives us perfect detection. Just remember the goldfish token memory, and that you might need to reprime the AI after a while.
## Refreshing ChatGPT's memory
If you notice that you start getting answers that stray too far off from things you have told ChatGPT earlier, it is a good idea to reprime the memory. This can be easily done by starting a new chat and sending the primer again. After that you can paste in the function you are working with, together with new instructions on how how you want the function to be modified.
## Research assistant
Another interesting use apart from generating code is to use it for research. Finding relevant information can be complicated, especially for more abstract concepts. ChatGPT is great for giving you a kickstart:
![](/docs/getting-started/ai-assisted-dev/chat-gpt/research-4.png)
In the same way we can easily ask follow-up questions:
![](/docs/getting-started/ai-assisted-dev/chat-gpt/research-1.png)
![](/docs/getting-started/ai-assisted-dev/chat-gpt/research-2.png)
![](/docs/getting-started/ai-assisted-dev/chat-gpt/research-3.png)
(This might not be the best example if there are any newer API's than 2021, or if anyone of them updated ther pricing, etc. It works for the most part though!)
## Age of exploration
While a lot is known about ChatGPT, there are still a lot of dark spots on the AI-map. New strategies, primers, tips and tricks are invented and uncovered every day. If you have found something that we havent touched upon here and feel like sharing it, please hop on in to the #chat-gpt channel on our [Discord server](https://discord.com/invite/23xU2hYrSJ). Exploration is more fun together, and we all stand on the shoulders of each other in this exciting new step within technology.

View File

@@ -0,0 +1,7 @@
---
hide_title: true
---
# AI Assisted Development
Noodl is a low code web app builder that will have you creating applications faster and smarter. Even though Noodl is technically a low code tool you can reduce the majority of coding using ChatGPT, making it much more accessible to no coders. Here we collect guides that target the different parts of Noodl where code is required and show you how to use ChatGPT to fill in the blanks. We are comitted to making Noodl accessible to no-coders without reducing the power and capabilities.

View File

@@ -0,0 +1,194 @@
---
title: REST API calls
hide_title: true
---
# Create REST API calls with Chat GPT
In Noodl you typically use the [REST](/nodes/data/rest) node to make REST API requests to access external services. This requires a little bit of coding to set up the request and to parse the response. It also requires a bit of knowlege to figure out how the API you want to use work and to read the documentaion.
<div class="ndl-image-with-background xl">
![](/docs/getting-started/ai-assisted-dev/rest/rest-node.png)
</div>
The example above is a non-trival REST API request. It takes the name of a wikipedia page and retrieves the main image of that page in a given size. This would certainly take me a few minutes to figure out how to do and it's quite tedious and non-interesting work. A [REST](/nodes/data/rest) node requires four important parts:
* **Endpoint** The HTTP endpoint of the REST API.
* **Method** The method of the API call, e.g. `POST` or `GET`.
* **Request script** This is a piece of javascript code that, given the node inputs, sets up all the parameters and content to be passed to the REST API call.
* **Response script** This is another javascript snippet that parses the received response and turns it into node outputs.
<div class="ndl-image-with-background l">
![](/docs/getting-started/ai-assisted-dev/rest/screenshot.png)
</div>
Let's see how we can use AI assisted development in Noodl to achieve this.
## Priming for REST node scripts
We will be using ChatGPT with GPT-4. The key is getting it to generate all the content we need for the REST node to work from just a single prompt. To do this we need to first provide the AI with the context, this is really the key to get it to work nicely with Noodl. This is called a _primer_, and here is one that we have been experimenting with for [REST](/nodes/data/rest) nodes:
````markdown
Hi ChatGPT. Here are your instructions. You must follow all of them.
- You will be writing Noodl in javascript functions for Noodl REST API calls.
- An input in a Noodl function must follow the format "Inputs.InputName".
- An input in a Noodl function is only read, never written to.
- An output in a Noodl function must follow the format "Outputs.OutputName = value".
- A variable in a Noodl function never stores an output.
- Inputs and Outputs in a Noodl function are global.
- Noodl functions do not use import statements.
- Noodl functions do not use export statements.
- Define constants as Noodl function inputs.
- You need to create two functions, one to prepare the REST API request and one to process the response.
- The function to prepare the request have the following format. It is called the "Request script". You don't need to wrap it in a function, just the javascript code.
```js
// All REST options should be set on the Request object as follows
// Put the headers needed for the API call in the headers object
// You don't need to set the content-type to application/json this is done automatically
Request.headers['authorization'] = "Bearer " + Inputs.APIKey;
// Put any query parameters needed for the API call in the parameters object
Request.parameters['limit'] = Inputs.NumberOfItems;
// If you are doing a POST method request, but the content in the content object
// No need to stringify, only a standard JSON object
Request.content = {
'param' : 'something'
}
```
- The function to parse the result of the REST API request have the following format. It is called the "Response script". You don't need to wrap it in a function, just the javascript code.
```js
// The content of the response is in the Response.content object
Outputs.Results = Response.content.results;
```
- Finally list the endpoint and the HTTP method in the following format. The endpoint can contain parameters using the {paramName} syntax.
Endpoint: https://example-endpoint.com/{userId}/fetch
Method: POST
Reply "Okidoki" if the instructions are clear, otherwise ask me to clarify
````
Copy and pasting this primer into ChatGPT GPT-4 and then following up with this prompt:
```
given a wikipedia page name, get the main image of that page
```
The nice little robot now gives us everything we need to prepare the REST node. After a quick description we get the request and response scripts we need:
<div class="ndl-image-with-background xl">
![](/docs/getting-started/ai-assisted-dev/rest/gpt-1.png)
</div>
We can simply copy and paste those into the corresponding properties of the REST node. Next we get the **Endpoint** and **Method** nicely listed for us. We also copy these parameters into REST node properties. It even provides us with a little bit of explaination of how to set up the inputs and how to use the outputs of the node.
<div class="ndl-image-with-background xl">
![](/docs/getting-started/ai-assisted-dev/rest/gpt-2.png)
</div>
We can now hook up the REST node as shown in the example above and simply put in a Wikipedia page name and provide a size of the image and we get the resulting URL back that we can connect to an image node.
## Refining your answer
Sometimes you will get an answer back that may be correct but you don't know exactly what the result is. You can always hook it up in Noodl and just test it, view the outputs and see if the result matches your expectations. Or you can simply ask Chat GPT. In this example I asked it to create a spotify API integration, the prompt was:
```
get the songs of a playlist using the spotify API
```
It provided me with everything I needed to copy and paste into the REST node.
<div class="ndl-image-with-background xl">
![](/docs/getting-started/ai-assisted-dev/rest/gpt-3.png)
</div>
But I wasn't sure about what the content of the output was, so I asked:
```
What is the format of an object in the songs output?
```
And it give me a rought outline of the object along with a few examples.
If you find that the inputs and outputs are not to your liking, or that it did not exactly do what you were asking for simply try providing it more context and ask it to change the code.
## Providing context
The ChatGPT models were trained on data up to 2021 so it's clearly missing some more recent APIs, such as actually it's own API. But you can provide context when asking it to generate code for you.
```
using this api https://platform.openai.com/docs/api-reference/chat/create I want to provide messages as an array of string and get the response back
```
<div class="ndl-image-with-background xl">
![](/docs/getting-started/ai-assisted-dev/rest/gpt-4.png)
</div>
It gives you a nice piece of code, but after copy&pasting it into my REST node and testing it on an exampt, it does not give the expected result. In this case I just get an array of `undefined`, thats weird. Lets see what ChatGPT thinks about that.
```
I just got an array of undefined back
```
It fixes the problem and gives you a new updated function. This works much better. That is pretty astounding!
<div class="ndl-image-with-background xl">
![](/docs/getting-started/ai-assisted-dev/rest/gpt-5.png)
</div>
Sometimes the REST call will fail and generate an HTTP error. If the error is not shown on the REST node you can find the error in the web debugger. If you are on the frontend click on the debug icon at the top bar:
<div class="ndl-image-with-background m">
![](/docs/getting-started/ai-assisted-dev/rest/debug-1.png)
</div>
If you are in a cloud function, open the debugger for the cloud function runtime:
<div class="ndl-image-with-background m">
![](/docs/getting-started/ai-assisted-dev/rest/debug-2.png)
</div>
In the debugger look for the **Network** tab:
<div class="ndl-image-with-background m">
![](/docs/getting-started/ai-assisted-dev/rest/network.png)
</div>
Any failed calls will generally be highlighted in red, you can find your endpoint and look at the response from the request. See if you can find an error message and let ChatGPT know about the error and try to have it fix it.
Have fun playing with Noodl and AI assisted development and let us know of your discoveries in the Discord community!

View File

@@ -0,0 +1,17 @@
---
title: Editor Tour
hide_title: true
---
# The Complete Editor Tour
<div style={{padding:'62.5% 0 0 0',position:'relative'}}><iframe width="100%" height="100%" src="https://www.youtube.com/embed/gbEKSQKocHc" style={{position:'absolute',top:0,left:0}} frameBorder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe>
</div>
<br/>
<br/>
:::note
Please note that this video was recorded for the earlier 2.3 version of Noodl. It features a few minor visual inconsistencies with newer versions, but no big changes for the purposes of this video.
:::

View File

@@ -0,0 +1,64 @@
---
title: Fundamentals
hide_title: true
---
import useBaseUrl from '@docusaurus/useBaseUrl'
# Fundamentals
Here you will learn about a few fundamental concepts in Noodl that are important to get a grasp on before continuing. You can view the video below or review the short guide.
<div style={{padding:'62.5% 0 0 0',position:'relative'}}><iframe width="100%" height="100%" src="https://www.youtube.com/embed/kD-Oz_M-IS4" style={{position:'absolute',top:0,left:0}} frameBorder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe></div>
## Nodes
The main building blocks of Noodl are nodes. Every node has its own specific purpose and is very simple in itself, but together they become really powerful. There are a number of types of nodes, indicated by its color. **Blue** nodes are visual elements, such as buttons, or not immediately visible but related, such as groups. **Green** nodes are generally related to reading, writing and manipulating data. **Grey** nodes are utilities, and **Pink** nodes are related to business logic and Javascript.
![](/docs/getting-started/basic-concepts/nodes.png)
### Inputs and outputs
All nodes have inputs and outputs. Connecting two nodes is as easy as clicking one node, dragging the connection to another node and selecting what output should be connected to what input. You can visually see the data flow in the Node Editor, and clicking a connection allows you to see what data is being passed.
![](/docs/getting-started/basic-concepts/connecting-nodes.gif)
Most properties in Noodl can be connected, that's what makes it so powerful and easy to be creative with.
### Connection types
Noodl has two different connection types, <span className="ndl-data">Data</span> and <span className="ndl-signal">Signals</span>.
- <span className="ndl-data">Data</span>: This connects an output value from one node, such as the content of a Text Input or a Variable, to an input of another node. This is typically used to present data in your user interface. When data is passed over a connection you will see it light up in the node graph editor.
- <span className="ndl-signal">Signal</span>: Whenever we want our app to perform some kind of action we use a Signal connection. This connects a signal output, a node can have several signal outputs that each will trigger on a specific event, e.g. Click on a button, to a signal input on another node. The receiving node will typically be some kind of action that is peformed when the signal is received. When a signal is triggered you will see it light up in the node grap editor.
### Type conversion
You can not connect <span className="ndl-data">Data</span> and <span className="ndl-signal">Signals</span> directly to one another, but there are nodes that can convert the connection types, for example if you want to trigger a signal whenever a value changes.
## Components
A clusters of nodes and connections, "node graph", and make up a component. These components can then be combined in new node graphs and be part of even larger components. For a larger app, you typically create many different components to keep your application organised. These components can have their own inputs and outputs, just like the built in nodes. This is a great way of reusing UI or logic across your application.
Component is also a great way to abstract and encapsulate complex functionality. You can decide what properties a component has and that are exposed the outer world, sort of like an API.
### Component types
There are four types of components: Page, Visual, Logic and Cloud Function.
- `Page`: Page components are your apps screens and can be navigated to using Page Router nodes.
- `Visual`: Visual components are made to group and render your UI elements on the screen. They can contain logic, but the main output is always something visible.
- `Logic`: Logic components are the brains of your app. They are used to group nodes that dont render anything on screen. It can be as simple as a couple of nodes that do data transformation to something more complex, like routing signals to different outputs based on passed values.
- `Cloud Function`: A cloud function component is a logic component that run in the cloud. It can do tasks that cannot be done on the frontend and may contain other logic components.
### Update everywhere
One very practical thing with Components is that they are global, meaning that whenever you update a component's internals, these changes will be updated everywhere this component is used in the app.
<div className="ndl-video">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/getting-started/basic-concepts/always-live.mp4")}/>
</div>

View File

@@ -0,0 +1,204 @@
---
title: Noodl AI
hide_title: true
---
# Noodl AI
Noodl AI presents a unique way of leveraging the power of Large Language Models (LLM) for app development. It allows you to generate custom building blocks from simple text prompts, simplifying the development process. By playing on, and amplifying, the strengths of current LLM models, Noodl AI goes beyond experiment and novelty, becoming truly valuable in real-world applications.
<div className="ndl-image-with-background l">
![](/docs/getting-started/noodl-ai/cover.png)
</div>
Easily create JavaScript logic, handle API calls, interact with databases, and generate UI elements. Our goal is to make the development process more inclusive and accessible for a bigger range of skill sets. Whether you're an experienced developer or a beginner, Noodl AI is here to support you throughout your app development journey.
> In the ever-evolving field of generative AI, Noodl AI keeps pace with the latest developments as well as the valuable feedback from the Noodl community. Please dont hesitate to reach out with your thoughts on our [Discord](https://discord.com/invite/23xU2hYrSJ) or [Twitter](https://twitter.com/getnoodl).
## Noodl AI access
All Noodl users have access to the beta version of Noodl AI, which includes generative features powered by OpenAIs GPT. However, as these models come with associated costs, we have divided the Noodl AI beta into two modes:
**Limited Beta** is free for all users and is based on GPT-3.5. As this model is not as advanced as GPT-4, this mode only supports very limited AI features.
**Full Beta** is based on GPT-4 and requires you to input a GPT-4 API key from OpenAI. This AI mode includes all generative features, and provides significantly better results.
:::note
Please note that the performance of the same AI command varies between the two modes due to the different capabilities of the GPT versions. For the best results, we recommend using the Full Beta mode, as it gives you a better output.
:::
### LLM agnostic
To streamline the beta phase, the public version of Noodl AI is limited to GPT-3.5/4. However, Noodl is built to leverage custom LLM endpoints, and even different models for compliance, regulations, performance, or price concerns.
> We are currently looking for teams willing to test custom solutions. Please contact us of you are interested in using another LLM, custom endpoint, or your own knowledge base.
## Setup Instructions
Any version of the editor that is version 2.9 or higher comes with the generative features enabled. You can see your current version during login, or in the top right corner of the editor. If your editor has not automatically updated to the latest version, visit the [Noodl Console](https://console.noodl.net) to download it manually.
When opening a project you should see the AI bar in the top left corner of the node canvas. In the Editor Settings (in the Sidepanel) you can find options for changing the beta mode, or disabling all AI features.
<div className="ndl-image-with-background l">
![](/docs/getting-started/noodl-ai/settings.png)
</div>
### Limited Beta
The Limited Beta mode is activated by default and is free to use for everyone. It is built on top of OpenAIs GPT-3.5 model. Due to the limitations of GPT-3.5 we only support the `/Function` command in this mode.
### Full Beta
The Full Beta mode requires an GPT-4 API key from OpenAI. If you dont have a GPT-4 API key you can request one from OpenAI (but there might be a waiting list). Noodl will not charge you for using the Full Beta, but Open AI will bill you their regular amount based on your API usage.
To activate the Full Beta, open the Editor Settings, find the **Noodl AI (Beta)** settings, and change the mode to `Full beta (gpt-4)`. Enter the API key and click **Verify** to activate.
## AI commands
When using Noodl AI, start by choosing the type of command you want to work with. This helps the AI generate the best possible result. Behind the scenes, the command sets up the AI with the relevant context and project information, allowing it to perform its task effectively.
Noodl AI has the following commands:
- `/Function`
- `/Read from database`
- `/Write to database`
- `/UI`
- `/Image`
### `/Function`
<div className="ndl-image-with-background">
![](/docs/getting-started/noodl-ai/function.png)
</div>
The `/Function` command is specialized in generating custom JavaScript functions. This is the most flexible and general purpose command. Noodl AI makes sure the generated code follow Noodl best practices, such as creating Inputs/Outputs on the node, checking the data sent to it, and sending Success and Failure signals. It also names the function, which is not necessary, but a nice touch for automatic self-documentation.
After the initial prompt, an AI Function node will be created in the currently active node graph. In its Property Panel you can find two tabs: **AI Chat** and **Properties**.
In the **AI Chat** tab you get an explanation of the generated code. You can also continue prompting the AI to refine your results. The chat history is saved so you can come back to it at any time to pick up where you left off or simply to remember what the function does in more detail.
<div className="ndl-image-with-background">
![](/docs/getting-started/noodl-ai/function-chat-panel.png)
</div>
The **Properties** tab is the same as the Property Panel for a regular Function node. Here you can inspect the generated code, and modify it by hand if you want to. The AI will take your modifications into account in future follow-up prompting.
<div className="ndl-image-with-background l">
![](/docs/getting-started/noodl-ai/function-properties-panel.png)
</div>
Here are some examples of prompting the `/Function` command:
- https://youtu.be/-9bd5AVo9o8
- https://youtu.be/8eOEhphQz6k
:::note
Due to GPTs limited token memory the AI can start hallucinating if the code becomes too long. This can lower the generated code quality. Consider splitting your big function into smaller functions that handle one task each. <br/><br/>
This also has the benefit of making it easier to prompt, minimizing the risk of the AI misunderstanding your instructions.
:::
### `/Read from database`
<div className="ndl-image-with-background">
![](/docs/getting-started/noodl-ai/read-db.png)
</div>
The `/Read from database` command is used to make queries to the connected Noodl database. Its primed with your database schema, and can therefore create complex queries from simple text prompts. It can also go beyond a regular database query, allowing you to do calculations and process the returned data if needed.
After the initial prompt, an AI Query node will be created in the currently active node graph. Technically, its built on top of the AI Function node created with the `/Function` command, but with different internal rules. From a user perspective it works in the same way though, having identical features in the [**AI Chat** and **Properties** tabs](#function).
Here are some examples of prompting the `/Read from database` command:
- https://youtu.be/CxdyIqMq8gE
- https://youtu.be/nr4BI_pvoFA
### `/Write to database`
<div className="ndl-image-with-background">
![](/docs/getting-started/noodl-ai/write-db.png)
</div>
The `/Write do database` command is used to create or update data in the connected Noodl database. It is primed with your database schema, and can therefore do complex updates to the database from simple text prompts. It can also go beyond a regular database modification, allowing you to process the data before it is saved if needed.
After the initial prompt, an AI Query node will be created in the currently active node graph. Technically, its built on top of the AI Function node created with the `/Function` command, but with different internal rules. From a user perspective it works in the same way though, having identical features in the [**AI Chat** and **Properties** tabs](#function).
Here are some examples of prompting the `/Write to database` command:
- https://youtu.be/nr4BI_pvoFA
### `/UI`
<div className="ndl-image-with-background l">
![](/docs/getting-started/noodl-ai/ui-command.png)
</div>
:::note
This is an experimental command and is still both limited and unpolished. Please reach out to us if you have any thoughts on future developments.
:::
The `/UI` command is used to generate visual nodes from a text prompt. Currently it supports the Group, Columns, Button, Text Input, Checkbox, Image and Dropdown nodes, with some limited styling. It can also generate components from your design system, if they are AI annotated.
After the initial prompt you will see nodes start to generate at the bottom of the visual tree. They are placed in a Group node with the dimension mode set to `Explicit width & content height`.
#### Annotating your components for AI use
You can help the `/UI` command generate more useful results by adding AI metadata to your visual components. As the `/UI` command is still in an experimental phase this is done manually with Noodls comment feature. Both the UX and the command performance will change in the future, but we still want to share our current progress to get feedback from the community.
AI metadata can be added to any visual component by following these steps, if you already have a design system in place, you can skip steps 1 and 2:
1. Create a visual component and open its node graph. Build and design it the way you see fit for your use case. You could use the `/UI` command for a quick boilerplate to start from.
2. Add a Component Inputs node with connections to the properties that should be changeable from outside of the component.
3. Open the Node Picker and create a new comment. Start with the text `AI:`.
4. Add a description of how the component should be used
5. Add a list of attributes to the comment. These represent all the Component Inputs that the AI should know about, and be able to change. Each attribute should exactly match the name of the Component Inputs property, followed by any possible options and values it can have. The AI will only touch properties listed here, and ignore the rest.
<div className="ndl-image-with-background xl">
![](/docs/getting-started/noodl-ai/annotation.png)
</div>
The `/UI` command will now consider your component when generating prompted layouts. This works by injecting your annotation into the command primer, so (as with most things GPT) you might find that you need to tweak the description for optimal results.
This method allows you fine grained control over your design system, while still leveraging the capabilities of LLMs for the bigger picture, giving you useful (and reusable) results in a real world setting.
> We are currently looking for teams willing to test AI annotations on their design systems. Please contact us of you are interested in UI generation with your medium/large component library.
### `/Image`
<div className="ndl-image-with-background l">
![](/docs/getting-started/noodl-ai/image-command.png)
</div>
:::note
This is an experimental command and is still both limited and unpolished. Please reach out to us if you have any thoughts on future developments.
:::
The `/Image` command creates a single Image node and populates it with a Dall-E generated image. The images are saved in the project folder.
> All your prompts will be sent to OpenAI. This includes any AI annotations in your components, as well as your data models (but not any of the data in your database). However, this will not be used to train OpenAIs models.<br/><br/>
> You can read more about OpenAIs data usage policy [here](https://openai.com/policies/api-data-usage-policies).
## Make your voice heard!
We strongly believe that our community is one of our greatest assets, and we value all the input and feedback that we get, as it helps us shape and steer our development efforts. That's why we have opened up our beta to invite the community to actively participate in the journey. We encourage you to share your thoughts, ideas, and suggestions with us on [Discord](https://discord.com/invite/23xU2hYrSJ) or [Twitter](https://twitter.com/getnoodl).
Thank you for trying out the new features, and happy noodling!

View File

@@ -0,0 +1,25 @@
---
hide_title: true
---
import { LinkButtonGrid } from '../../src/blocks/LinkButtonGrid.tsx'
# How Noodl works
Noodl has two fundamental parts:
1. **A visual builder for modern web application frontends.** You compose frontends from a library of UI Controls that are highly customizable, down to fine grained animations and transitions. A versatile navigation system that supports both simple and more complex nested navigation and popups. Re-usable components to support dynamic, reactive interfaces. And you build logic either visually or with simple Javascript functions.
2. **Integrated cloud services.** A solid easy to learn database where you manage classes and records. You can of course also query data and create record relations. The cloud database also includes user management (sign up, log in/out) and Access Control. You can create cloud functions, just like building logic on the client, that run in the cloud for background jobs, compound queries, security etc. Noodl can host the cloud services for you or you can host it on your own. It is based on a popular open source project ([Parse](http://parseplatform.org)).
## Getting started
Getting started with Noodl is easy. There are interactive lessons built into the tool that are a great starting point. In the documentation we recommend the following articles to get started.
- First we recommend reviewing the [Workflow overview](workflow) page and then the [Fundamentals](fundamentals) page to get some more meat the bones in terms of what Noodl can do for you.
- Then dive into the [Guides](/docs/learn) section and start digging into the wonderful world on Noodl.
Also don't forget to check out our [Community channels](https://www.noodl.net/community) - great ways to learn and make new friends!
Happy Noodling!

View File

@@ -0,0 +1,155 @@
---
title: Workflow overview
hide_title: true
---
import useBaseUrl from '@docusaurus/useBaseUrl'
# Workflow Overview
Let's take a look at the different concepts of the Noodl workflow when building your applications, namely
* Building User Interfaces
* Page Navigation & Components
* Actions
* Working with Data
* The Cloud Database
* Business Logic
* Cloud Functions
* Collaboration
* Modules and Prefabs
## Building user interfaces
Any app needs a great **User Interface**. In Noodl it's easy and fast to build dynamic, reactive, beautiful user interfaces using the built in UI Controls. These controls are then arranged and layed out appropriately. Noodl contains a library of highly customizable core UI controls. It's also possible to extend Noodl with your own UI Controls, if needed.
<div className="ndl-video">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/getting-started/basic-concepts/ui-1.mp4")}/>
</div>
### Customizing UI Controls
Each UI Control have properties that use can be used for fine grained customization. Here is a short overview of concepts important for building user interfaces:
- **Visual States** Each UI Control have a set of visual states, e.g. Hover, Pressed etc, and the control properties can be set for each state individually. Learn more [here](/docs/guides/user-interfaces/visual-states).
- **Transitions** You can control transition animations between each visual state using the animation editor.
- **Variants** Define re-usable variants for your UI controls including properties, visual states and transitions. Build your own design systems and become super productive. Learn more [here](/docs/guides/user-interfaces/style-variants)
## Page Navigation & Components
When you start building more complete frontends you will be working with **components**. A component can be an entire page in your application, these are called **Page Component**s, or they can be a smaller part of your UI that you want to re-use in many places, these are called **Visual Component**s. Pages can be found in the top navigation bar, and visual components are found in the component panel in the sidebar. You can also use the preview in design mode to quickly locate a certain visual element in your app.
<div className="ndl-video">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/getting-started/basic-concepts/nav-1.mp4")}/>
</div>
It's simple to create page navigation but the system is also flexible enough to build more complex navigation flows, such as nested navigation. Noodl supports state of the art web app navigation, with permalinks and encoding of data in URLs. Here is a short overview of concepts important for building pages and navigation:
- The **Page**, **Page Router** and **Navigate** nodes are the essence of the navigation system. Learn how to build basic page navigation in this [guide](/docs/guides/navigation/basic-navigation) and more advanced multi level navigation [here](/docs/guides/navigation/multi-level-navigation).
- Another feature of the navigation is **Popups**, these can be used to show or collect transient information. Learn more about popups [here](/docs/guides/navigation/popups)
## Actions
An important concept in Noodl, that was briefly shown above, is **action nodes**. These nodes perform some sort of action when they are triggered by a signal, e.g. from a UI control (such as a button click) or from another action node (such as when the action has completed successfully or failed). Noodl contains a wide variety of action nodes for most common basic tasks such as **Navigation** and **Reading and writing data**.
![](/docs/getting-started/basic-concepts/connecting-nodes.gif)
Connections between nodes is a core concept in Noodl, this is how you connect your user interface to data and actions.
## Working with data
When you have built some of your frontend, added pages, components and navigation it is time to connect dynamic data to your user interfaces. This is what makes your application come alive, and in Noodl you have a neat visual way to build modern reactive user interfaces.
<div className="ndl-video">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/getting-started/basic-concepts/data-1.mp4")}/>
</div>
There are three basic concepts for data in Noodl, the **Variable**, **Object** and **Array** nodes. You can learn more about how to work with these [here](/docs/guides/data/overview).
A very common pattern in web applications frontends are different types of lists or other dynamic repeating content. Learn how to do that in Noodl [here](/docs/guides/data/list-basics)
## Business Logic
As mentioned above you use **action nodes** to perform some sort of business logic action in Noodl. There are a wide variety of action nodes built in for most basic tasks such as navigating between pages, or storing data in the database. Data and signals from your UI controls are connected to your action nodes, and you can connect them together to make flows.
<div className="ndl-video">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/getting-started/basic-concepts/actions-1.mp4")}/>
</div>
But most applications require some sort of more advanced business logic, for instance processing data from the database or maybe logic to create more advanced UI components. You can build most basic logic with the built in action nodes, but when you require more complex actions it is very easy and fast to add code to your projects.
<div className="ndl-video">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/getting-started/basic-concepts/edit-code.mp4")}/>
</div>
Not a skilled software developer? Don't worry, the philosofy of Noodl is to focus on the code that matters and remove much of the unncessecary complexities of developing software. If you have basic knowledge of Javascript, you will get very far with Noodl.
## The cloud services
Now it's time to take a look at the second part of the Noodl platform, the cloud services. This is an important piece of any web application. You can create and manage cloud databases, or cloud services as they are refered to as, from within your Noodl project.
<div className="ndl-video">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/getting-started/basic-concepts/cloud-1.mp4")}/>
</div>
The Noodl cloud services will provide your applications with a couple of important functions:
- **Reading and writing data**. You can create **Class**es where you store **Record**s, these can then be read, filtered and presented in your application. Dive in [here](/docs/guides/cloud-data/overview) to start learning about cloud services.
- **User Management**. Most applications need users, you can sign up users, log in, manage passwords etc.
- **Access Control**. When you have users and data, you need to control which users can access what data. This can be done trough **Role**s in your cloud servies. Learn more [here](/docs/guides/cloud-data/access-control)
- **Cloud Functions**. You are not just limited to creating logic on the frontend, you can also create cloud functions that peform tasks that run in the cloud. This is super useful for things like database operations that should have admin access and background jobs.
## Cloud Functions
Using the same techniques to build logic as described above you can build logic that runs in the cloud, this is called a **Cloud Function**.
<div className="ndl-video">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/getting-started/basic-concepts/cloud-2.mp4")}/>
</div>
This is very useful for many different functions such as:
- Access the database in admin mode, do things that should not be possible from the client for security reasons such as resetting passwords and verifying emails.
- Connection to external services using e.g. OAuth or webhooks need to logic to run in the cloud, here cloud functions can be super helpful.
- Performing queries is much faster in the cloud, so if you need to make many database queries and compound the result you can achieve high performance by doing this in the cloud.
## Collaboration
Finally, when your applications is growing you will want to work with your colleagues and friends. Thankfully, Noodl has a solid collaboration system with version control built in. You can work together on a project, you can each work on your own branches and merge. We try to bring really powerful concepts from the software development world into Noodl to make sure that you will never hit a wall.
<div className="ndl-image-with-background">
![](/docs/guides/collaboration/version-control/initial-state.png)
</div>
Learn more about the collaboration and version control capabilities [here](/docs/guides/collaboration/version-control).
## Modules and Prefabs
In Noodl you can also import modules developed by others or develop your own modules. You can find a list of current modules provided by Noodl [here](/library/modules/overview). You can also build your own modules and extensions, for example wrapping existing Javascript libraries. You can read more about it [here](/javascript/extending/overview).
<div className="ndl-image-with-background l">
![](/docs/guides/user-interfaces/modules/browse-modules.png)
</div>
An important concept in Noodl that will take your productivity to the next step are **prefabs**, these are pre-built components that can be cloned into your project. You can use them as is, or as a starting point to modify and extend. It's also a great way to learn some of the good development patterns in Noodl.
<div className="ndl-image-with-background l">
![](/docs/guides/user-interfaces/modules/browse-prefabs.png)
</div>

View File

@@ -0,0 +1,613 @@
---
title: Building Business Logic Using Javascript
hide_title: true
---
import CopyToClipboardButton from '/src/components/copytoclipboardbutton'
import ImportButton from '../../../src/components/importbutton'
# Client Side Business Logic Using Javascript
## What you will learn in this guide
This guide shows how you use Javascript to implement business logic on the client (front-end) side of your app. While you can build business logic with a no-code approach using logic nodes (as described in [this guide](/docs/guides/business-logic/client-side-biz-logic-nodes)) its often easier to use Javascript since business logic sometimes is more readable in code form. In this guide we will make use of the [Function](/nodes/javascript/function) and [Script](/nodes/javascript/script) nodse which are the main way to mix code and no-code in Noodl.
Another important node is the [Component Object](/nodes/component-utilities/component-object) that's the main way of storing state data so it can be accessed easily both in the code world and the node world.
## Overview
The guide will implement a simple multiselect interaction on a list and a few multiselect operations, such as delete and copy. The business logic will handle the multi-select itself (selecting, deselecting) as well as the operations on the content in the list. It will also control the state of **Buttons** on the screen.
We will use various Noodl concepts, such as lists, Arrays and Objects, so it's probably good if you have gone through those guides before ( [List guide](/docs/guides/data/list-basics), [Array guide](/docs/guides/data/arrays) and [Object guide](/docs/guides/data/objects)). We will also use Javascript of course, and especially many Array functions, so that's worth checking out as well. A good documentation can be found for example [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array).
There is also a dedicated guide on the **Script** node, [here](/docs/guides/business-logic/javascript).
## Creating a list with multiselect UI
This example will revolve around a simple collection of orders, for example in a e-commerce system. The orders have an `order_nbr`, a `quantity` and a `delivery_date`. The App should list the orders and let the user select a number of them, using a standard multi select interaction. Then the selected orders can either be _deleted_, _copied_ or _merged_. We will look what the actual operations mean in more detail as we implement them. It's also possible to _select all_ or _deselect all_ items.
## The base data
We start this guide by adding in our base dataset that we want to work on. We are going to use the [Static Array](/nodes/data/array/static-array) to keep our data, but in essence this data could have come from a cloud database. So start a new project, using the "Hello World" template. Then add a **Static Array** node, set it to `JSON` format, and add in the following data:
```json
[
{"order_nbr":"A-12124", "quantity":2, "delivery_date":"2022-10-23"},
{"order_nbr":"A-26232", "quantity":6, "delivery_date":"2022-10-25"},
{"order_nbr":"V-23532", "quantity":3, "delivery_date":"2022-09-13"},
{"order_nbr":"B-99243", "quantity":5, "delivery_date":"2022-08-03"},
{"order_nbr":"V-35124", "quantity":1, "delivery_date":"2022-12-20"},
{"order_nbr":"G-23421", "quantity":1, "delivery_date":"2022-09-09"},
{"order_nbr":"B-86612", "quantity":8, "delivery_date":"2022-11-21"},
{"order_nbr":"C-61633", "quantity":5, "delivery_date":"2022-05-29"},
{"order_nbr":"V-42241", "quantity":2, "delivery_date":"2022-11-15"},
{"order_nbr":"V-99112", "quantity":12, "delivery_date":"2022-12-20"},
{"order_nbr":"A-51512", "quantity":1, "delivery_date":"2022-07-07"},
{"order_nbr":"B-00914", "quantity":8, "delivery_date":"2022-09-13"},
{"order_nbr":"C-11121", "quantity":9, "delivery_date":"2022-10-19"}
]
```
Now we have some orders in the system so ket's start building the UI. We will start with the multi select list.
## Building a Multi Select List
We want to build a component that can show an array of items, as well as keeping track of which items that are selected. Let's outline the functionality of it below
* You should be able to feed it with items that will be shown in the list, with a checkbox in front of them
* You should be able to feed it an initial selection, i.e. which items that should be selected
* As you selected or deselect items, the component should trigger a signal and also provide an **Array** with the items that are currently selected
* You should be able to select or deselect all items by triggering a signal
A question is how the multi select list would encode which items that are selected. One way of doing it would be that the multiselect list would provide an **Array** of items of format:
```json
{
"Value":<id of selected object>
}
```
So for example, if the list has three items, with ids "111", "222" and "333". If the first and second item are selected, the output from the component would be:
```json
[
{
"Value":"111"
},
{
"Value":"222"
}
]
```
So, when we are done, the component would look something like below from the outside:
<div className="ndl-image-with-background">
![](/docs/guides/business-logic/client-side-biz-logic-js/multiselect-list-1.png)
</div>
### Selection logic
Let's think about the logic needed by the component. Internally the component would need to keep track of which items that are selected and which are not. Partly to be able to generate the **Array** above, but also to be able to visualize it correctly.
You could of course store the selection state directly in the item, i.e. an order in our case, but that's actually not a great design. What would happen if the order was part of multiple Multi Select lists? Also, if we end up storing the item in the database we might also store the selection state if we are not careful, which doesn't make sense at all.
A better design is to "wrap" the each order in another **Object** that keeps track of the selection state. Then we display a list of those objects when we present the list instead of the original items. The "wrapper" of course needs to store the **Id** of the **Object** that it wraps so it can present the data from the item too. The format of the wrapper could be:
```json
[
{
"Checked":<true or false depending if the item is checked or not>,
"Value":<the id of the Object it wraps>
}
]
```
So, again in the example above with the items with id "111", "222" and "333", with the first two checked, the Checkbox array would look like the following:
```json
[
{
"Checked":true,
"Value":"111"
},
{
"Checked":true,
"Value":"222"
},
{
"Checked":false,
"Value":"333"
},
]
```
So in summary our Multi Select list component will need the following logic on the inside
* When the list is fed with new items, it will create one new Checkbox Array containing an **Object** per item (the wrapper) and set `Checked` to `true` or `false` depending on initial selection.
* The list item presenting each item will use the `Checked` property to visualize the checked status of the item. If the user clicks the checkbox the `Checked` property is toggled.
* Whenever the checked status changes, the Multi Select List should generete a new list that only holds the items that are checked, according to the format above, i.e. `[{"Value":<id of a checked item>}, ...]`. This will be presented to the outside world.
Start by creating a new visual component. Call it "Multi Select List".
<div className="ndl-image-with-background">
![](/docs/guides/business-logic/client-side-biz-logic-js/component-1.png)
</div>
You will now have a more or less empty component. Add in a **Component Inputs** and a **Component Outputs**. We now what they should contain are already, i.e. how to interact with our component. So in the **Component Inputs** add the ports `Selection`, `Items`, `Select All` and `Clear Selection`.
<div className="ndl-image-with-background">
![](/docs/guides/business-logic/client-side-biz-logic-js/component-inputs-1.png)
</div>
In the **Component Outputs** we create the ports `Selection` and `Selection Changed`.
<div className="ndl-image-with-background">
![](/docs/guides/business-logic/client-side-biz-logic-js/component-outputs-1.png)
</div>
Now we begin writing our Javascript logic. Add a **Script** node.
<div className="ndl-image-with-background">
![](/docs/guides/business-logic/client-side-biz-logic-js/multiselect-list-2.png)
</div>
We begin by adding two inputs, one is the `Items` that the list should display. The other is the `InitialSelection` that contains any items that should be selected initially by the list. Both of these are of `Array` type.
<div className="ndl-image-with-background">
![](/docs/guides/business-logic/client-side-biz-logic-js/multiselect-script-1.png)
</div>
Ok, let's start coding! The first thing we want to do is to generate our Checkbox list, i.e. the **Array** of wrapping **Objects** that hold the checked state of each item.
## Setter functions - execute code when an input changes
We want to generate that **Array** any time the `Items` input changes. If you want to execute a piece of script any time a specific input changes you can use a _Setter_ function. These are declared using the following format:
```javascript
Script.Setters.<property name> = function (value) {...}
```
In our case we want to generate the Checkbox **Array** whenever **Items** is changed so we add the follwing code in our script file:
```javascript
Script.Setters.Items = function (items) {
if(!Script.Inputs.Items) return
// Create a list of all items with checked status, based on the initial selection array
Component.Object.Checkboxes = Script.Inputs.Items.map(o => Noodl.Object.create({
id:Component.Object.id+'-'+o.id,
Value:o.id,
Checked:Script.Inputs.InitialSelection!==undefined && !!Script.Inputs.InitialSelection.find(s => s.Value === o.id)
}))
}
```
This code goes through the incoming **Items** (referred to through `Script.Inputs.Items`), and for each item creates a new wrapper **Object** (using `Noodl.Object.create`). It checks the checked state and sets either `true` or `false` against the initial selection array. It also stores a pointer to the wrapped **Object** in the property `Value`.
### Checking for `undefined` inputs
If an input of a script node hasn't been set yet, for example in the case of our `Items` input or `InitialSelection` here, they will have the value `undefined`. It's often a good idea to explicitly check for this case in your code.
Another little trick here is that we explicitly set the **id** of the wrapper **Object**. This is actually not needed, as Noodl will generate a new **id** if no **id** is specified. But by making up an **id** of our own, that will be unique but the same for every wrapper object that wraps a specific object, we will reuse old **Objects** rather than creating new.
As you can see, the resulting **Array** is stored in `Component.Object.Checkboxes`. This we should pay some extra attention to.
## Using a Component Object in your scripts
Each component in Noodl have a **Component Object** that's unique. Opposite to a regular **Object** a **Component Object** _can only be accessed by the component itself, or by children of the component using **Parent Component Objects**_. This creates a scope and you does not risk other components accessing this object by mistake. For example if you would show two Multi Select lists on the same screen, if you used a regular **Object** to store your Checkboxes, you would need to make sure the **Objects** had unique ids (the Checkboxes can be different in the two lists). We avoid this problem all togheter by storing our Checkboxes in our **Component Object**.
All components have a Component Object, and you can access it in the Node world by adding a **Component Object** node. So close the **Script** node for now, and add in a **Component Object** node. Also add the property **Checkboxes** on the **Component Object**.
While we are at it we also connect the two outputs from **Component Inputs** (`Items` and `Selection`) to **Items** and **Initial Selection** inputs on the **Script** node.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/client-side-biz-logic-js/multiselect-list-3.png)
</div>
Let's test what we done so far, so we add in an **Array** node and connect the **Checkboxes** property of the **Component Object** to it (through the **Items** property). Next, we add in the `Multi Select List` component into our main App. Give it some items by connecting the **Static Array** items to it. If all is set up correctly, you should see some Checkbox items flowing into the **Array**.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/client-side-biz-logic-js/multiselect-list-4.png)
</div>
## Making the List Items
Now we are ready to create the List Items. Let's package up our `Multi Select List` components into a folder. So create a new folder, call it `Multi Select List`. Move our `Multi Select List` component into it. Then add another visual component. Call it `List Item`.
<div className="ndl-image-with-background">
![](/docs/guides/business-logic/client-side-biz-logic-js/component-2.png)
</div>
We keep the list item visually simple. A horizontal layout with a [Checkbox](/nodes/ui-controls/checkbox) and three texts. Add some padding and margins to make it a little prettier. If you want, you can copy and paste the nodes into your component.
<div className="ndl-image-with-background l">
<CopyToClipboardButton json={{"nodes":[{"id":"5c3f112c-604f-5c89-f500-bf8c79701ebc","type":"Group","x":-106,"y":-21,"parameters":{"flexDirection":"row","paddingTop":{"value":10,"unit":"px"},"paddingBottom":{"value":10,"unit":"px"},"paddingLeft":{"value":10,"unit":"px"},"paddingRight":{"value":10,"unit":"px"},"sizeMode":"contentHeight"},"ports":[],"children":[{"id":"8bfcaedb-94c0-e145-f973-5627c4adc2c8","type":"net.noodl.controls.checkbox","x":-86,"y":25,"parameters":{"useLabel":false,"marginRight":{"value":9,"unit":"px"}},"ports":[],"children":[]},{"id":"4abf508d-30fd-8273-0b5c-1c43ffc761fb","type":"Text","label":"Order nr","x":-86,"y":71,"parameters":{},"ports":[],"children":[]},{"id":"66a0a3b1-f67c-37fe-26ab-47df859823e7","type":"Text","label":"Quantity","x":-86,"y":131,"parameters":{},"ports":[],"children":[]},{"id":"80e557b6-964a-a9bb-87d4-13b4bf7ee1d5","type":"Text","label":"Delivery date","x":-86,"y":191,"parameters":{},"ports":[],"children":[]}]}],"connections":[],"comments":[]}} />
![](/docs/guides/business-logic/client-side-biz-logic-js/listitem-1.png)
</div>
Then we hook up the data. We need to keep two **Objects** in mind this time. We have the **Object** providede by the **Repeater** - the "wrapper", and the actual order. We hook it up like below. We also add a **Component Output** to we can know if a Checkbox was clicked.
<div className="ndl-image-with-background l">
<CopyToClipboardButton json={{"nodes":[{"id":"176161a9-4c47-55e4-411e-73235b45899a","type":"Group","x":-106,"y":-21,"parameters":{"flexDirection":"row","paddingTop":{"value":10,"unit":"px"},"paddingBottom":{"value":10,"unit":"px"},"paddingLeft":{"value":10,"unit":"px"},"paddingRight":{"value":10,"unit":"px"},"sizeMode":"contentHeight"},"ports":[],"children":[{"id":"383c1752-5771-22f5-1334-160bdc0a7d99","type":"net.noodl.controls.checkbox","x":-86,"y":25,"parameters":{"useLabel":false,"marginRight":{"value":9,"unit":"px"}},"ports":[],"children":[]},{"id":"484c1964-a75f-c9f2-cdb9-2544a4b215d7","type":"Text","label":"Order nr","x":-86,"y":127,"parameters":{},"ports":[],"children":[]},{"id":"cbe0804c-f3d2-e3bc-8773-ed005e88d4f8","type":"Text","label":"Quantity","x":-86,"y":223,"parameters":{},"ports":[],"children":[]},{"id":"68fdebf1-67ee-702f-23cb-21d5b240f3a2","type":"Text","label":"Delivery date","x":-86,"y":319,"parameters":{},"ports":[],"children":[]}]},{"id":"739ecad6-ec0f-9f2b-aeaa-4bae353e7570","type":"Model2","label":"Wrapper","x":-583.3416811729484,"y":163.33900090176894,"parameters":{"idSource":"foreach","properties":"Checked,Value"},"ports":[],"children":[]},{"id":"5e697dd5-760b-b449-dcbd-1bd2629cc786","type":"Component Outputs","x":455.8443558682088,"y":170.21729798352135,"parameters":{},"ports":[{"name":"Selection Changed","plug":"input","type":{"name":"*"},"index":1}],"children":[]},{"id":"cdd8417c-4d43-2bbe-aaa9-e23a22c8fb04","type":"SetModelProperties","x":208.84435586820882,"y":169.21729798352135,"parameters":{"properties":"Checked"},"ports":[],"children":[]},{"id":"0b1e1592-c722-9af7-9903-206b05af8cdd","type":"Model2","label":"Order","x":-321.1860370411575,"y":104.12170291824759,"parameters":{"properties":"order_nbr,quantity,delivery_date"},"ports":[],"children":[]}],"connections":[{"fromId":"739ecad6-ec0f-9f2b-aeaa-4bae353e7570","fromProperty":"id","toId":"cdd8417c-4d43-2bbe-aaa9-e23a22c8fb04","toProperty":"modelId"},{"fromId":"739ecad6-ec0f-9f2b-aeaa-4bae353e7570","fromProperty":"prop-Checked","toId":"383c1752-5771-22f5-1334-160bdc0a7d99","toProperty":"checked"},{"fromId":"383c1752-5771-22f5-1334-160bdc0a7d99","fromProperty":"checked","toId":"cdd8417c-4d43-2bbe-aaa9-e23a22c8fb04","toProperty":"prop-Checked"},{"fromId":"383c1752-5771-22f5-1334-160bdc0a7d99","fromProperty":"onChange","toId":"cdd8417c-4d43-2bbe-aaa9-e23a22c8fb04","toProperty":"store"},{"fromId":"cdd8417c-4d43-2bbe-aaa9-e23a22c8fb04","fromProperty":"stored","toId":"5e697dd5-760b-b449-dcbd-1bd2629cc786","toProperty":"Selection Changed"},{"fromId":"739ecad6-ec0f-9f2b-aeaa-4bae353e7570","fromProperty":"prop-Value","toId":"0b1e1592-c722-9af7-9903-206b05af8cdd","toProperty":"modelId"},{"fromId":"0b1e1592-c722-9af7-9903-206b05af8cdd","fromProperty":"prop-order_nbr","toId":"484c1964-a75f-c9f2-cdb9-2544a4b215d7","toProperty":"text"},{"fromId":"0b1e1592-c722-9af7-9903-206b05af8cdd","fromProperty":"prop-quantity","toId":"cbe0804c-f3d2-e3bc-8773-ed005e88d4f8","toProperty":"text"},{"fromId":"0b1e1592-c722-9af7-9903-206b05af8cdd","fromProperty":"prop-delivery_date","toId":"68fdebf1-67ee-702f-23cb-21d5b240f3a2","toProperty":"text"}],"comments":[]}} />
![](/docs/guides/business-logic/client-side-biz-logic-js/listitem-2.png)
</div>
We add in a **Repeater** node in our `Multi Select List` component, pick our newly created list item as the item template. Finally we connect the **Checkboxes** output on our **Component Object** to the **Items** input of the repeater. We have a list!
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/client-side-biz-logic-js/multiselect-list-4.png)
</div>
## Triggering Signals in a Script
Now it's time that we write the script that will generate our selection list, i.e. an **Array** that will contain all the items that are currently checked. As you may remember, we decided on this format:
```json
{
"Value":<id of selected object>
}
```
We go back into our Multiselect Logic Script node and start by adding in a new function. Call it `updateSelection`. It will look like below:
```javascript
function updateSelection () {
Component.Object.Selection = Component.Object.Checkboxes.filter(o => o.Checked)
.map(o => Noodl.Object.create({
id:Component.Object.id+"_"+o.Value,
Value:o.Value}
));
}
```
Again, we are using our **Component Object** to store our **Array**. The **Array** is generated by walking through the Checkboxes **Array** and look for checked items. We then make a new **Object** (or recycling an old one if the **id** already exists) storing the **id** of the checked item in **Value**.
## Array outputs used in lists
:::note
While you can re-use the **Array** that contains the old items and just modify it, this will not be recognized as a change by the **Repeater** node (or any other node for that matter) and not automatically trigger it to update. Technically the **Array** didn't change - only it's contents. That's why it's important that you actually make a new **Array** and assigning it to the output, rather than modifying the existing one, unless you want to manually trigger **Repeaters** etc, using the **Array** to update. Generally the Javascript Array functions (filter, map, find, etc) will return new **Arrays** which makes them very easy to use in these cases.
:::
We actually need to call `updateSelection` in our Setter function for `Items` from before so right after we created the **Checkboxes** Array, so we immedieately get the Selection **Array** set up. So we update it like below:
```javascript
Script.Setters.Items = function (items) {
if(!Script.Inputs.Items) return
// Create a lits of all checked items, based on the initial selection array
Component.Object.Checkboxes = Script.Inputs.Items.map(o => Noodl.Object.create({
id:Component.Object.id+'-'+o.id,
Value:o.id,
Checked:Script.Inputs.InitialSelection!==undefined && !!Script.Inputs.InitialSelection.find(s => s.Value === o.id)
}))
updateSelection ();
}
```
We also need to be able to trigger the `updateSelection` whenever a user clicks a checkbox, regenerating the list. We can add __signals__ to our scripts by writing `Script.Signals.<Signal name>`. These signals will become inputs on the **Script** node that we can trigger. So our signal be named `UpdateSelection` and will look like this:
```javascript
Script.Signals.UpdateSelection = function () {
updateSelection ();
}
```
We can now connect the outgoing signal `Selection Changed` from our **Repeater** (which is triggered from the **Checkbox** in the list item) to this new signal. We also connect the `Selections` **Array** in our **Component Object** to our **Component Output** in the `Multi Select List`.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/client-side-biz-logic-js/multiselect-list-6.png)
</div>
Try clicking different Checkboxes and you should see the `Selection` **Array** change accordingly.
## Timing of signals
There is a timing aspect that's important to look into. Our `Multi Select List` should send out an event whenever the selection changes, together with the new selection array. If we directly connect the `Selection Changed` signal from the **Repeater** node to the **Component Output** we cannot be certain that the **Script** node has executed once the signal is triggered. It may or may not be the case. So anyone listening to the **Selection Changed** signal of the `Multi Select List` that also reads the new selection ( a very likely case) may read the previous **Array**.
The solution is to chain these events. The **Selection Changed** event should trigger the `UpdateSelection` signal on the **Script** node, then once the new selection **Array** is generated we need to send a signal from the **Script** that then will be used as the output signal from the component.
## Sending signals from a script
Let's add that signal. Click on the **Script** node. Then add a new output. Call it `SelectionChanged`.
<div className="ndl-image-with-background">
![](/docs/guides/business-logic/client-side-biz-logic-js/script-1.png)
</div>
Then make sure it's set up as a **Signal**.
<div className="ndl-image-with-background">
![](/docs/guides/business-logic/client-side-biz-logic-js/script-2.png)
</div>
Now we can send the signal from within our **Script** by calling it as a function `Script.Outputs.<signal name> ()`. In our case we add it in i our `updateSelection` function, as below:
```javascript
Component.Object.Selection = Component.Object.Checkboxes.filter(o => o.Checked)
.map(o => Noodl.Object.create({
id:Component.Object.id+"_"+o.Value,
Value:o.Value}
));
Script.Outputs.SelectionChanged ();
```
We can now connect our new output `SelectionChanged` to the `Selection Changed` input on the **Component Outputs**.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/client-side-biz-logic-js/multiselect-list-7.png)
</div>
## Finishing off the Multi Select List
The only thing left now is to implement the Select all / Clear selection functionality. This will be very easily done. We add two more incoming signals in our **Scrip** node, `Script.Signals.SelectAll` and `Script.Signals.DeselectAll`.
Adding implementing them in our **Script** node is easy and the full code now looks like below:
```javascript
function updateSelection () {
Component.Object.Selection = Component.Object.Checkboxes.filter(o => o.Checked)
.map(o => Noodl.Object.create({
id:Component.Object.id+"_"+o.Value,
Value:o.Value}
));
Script.Outputs.SelectionChanged ();
}
Script.Setters.Items = function (items) {
if(!Script.Inputs.Items) return
// Create a lits of all checked items, based on the initial selection array
Component.Object.Checkboxes = Script.Inputs.Items.map(o => Noodl.Object.create({
id:Component.Object.id+'-'+o.id,
Value:o.id,
Checked:Script.Inputs.InitialSelection!==undefined && !!Script.Inputs.InitialSelection.find(s => s.Value === o.id)
}))
updateSelection ();
}
Script.Signals.UpdateSelection = function () {
updateSelection ();
}
Script.Signals.SelectAll = function () {
if(!Component.Object.Checkboxes) return
Component.Object.Checkboxes.forEach ( o => o.Checked = true);
updateSelection ();
}
Script.Signals.DeselectAll = function () {
if(!Component.Object.Checkboxes) return
Component.Object.Checkboxes.forEach ( o => o.Checked = false);
updateSelection ();
}
```
We of course also need to trigger these functions from the **Component Inputs** ports, `Select All` and `Clear Selection`.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/client-side-biz-logic-js/multiselect-list-8.png)
</div>
We are done with the multi select list. Time to build the multi select operations we want to use it for.
## Adding in the multi select operations
As described in the beginning, we want to implement the operations _merge_, _delete_ and _copy_. We also want to be able to select all or clear all selections. So we need to build some UI. We will create a top bar that consist of a **Checkbox** and three **Buttons**. We do it as simple as possible. A horizontally laid out **Group** node with some margins and padding to make it easy to read. You can copy the nodes below if you don't want to build it yourself.
<div className="ndl-image-with-background l">
<CopyToClipboardButton json={{"nodes":[{"id":"d4c94e36-5463-a399-9dd0-69778d1236d8","type":"Group","label":"Top Bar","x":20,"y":46,"parameters":{"flexDirection":"row","sizeMode":"contentSize","alignX":"left","marginBottom":{"value":10,"unit":"px"},"paddingLeft":{"value":10,"unit":"px"}},"ports":[],"children":[{"id":"3aceb58c-1d9e-6511-d418-de806a995c4c","type":"net.noodl.controls.checkbox","x":40,"y":106,"parameters":{"useLabel":false,"alignY":"center","marginRight":{"value":10,"unit":"px"}},"ports":[],"children":[]},{"id":"0544f9ec-a773-5093-5b5c-296a800ee3fb","type":"net.noodl.controls.button","label":"Delete","x":40,"y":152,"parameters":{"marginRight":{"value":10,"unit":"px"},"label":"Delete"},"ports":[],"children":[]},{"id":"7d8801c3-c828-8956-0feb-f78ae9599de9","type":"net.noodl.controls.button","label":"Merge","x":40,"y":212,"parameters":{"marginRight":{"value":10,"unit":"px"},"label":"Merge"},"ports":[],"children":[]},{"id":"01ef643f-ccfb-1242-8155-47f4d1dfc9cf","type":"net.noodl.controls.button","label":"Copy","x":40,"y":272,"parameters":{"label":"Copy"},"ports":[],"children":[]}]}],"connections":[],"comments":[]}} />
![](/docs/guides/business-logic/client-side-biz-logic-js/app-1.png)
</div>
We can now connect the Select and Clear selection logic to our multi select list. We will use a **Switch** to trigger the two signals we implemented.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/client-side-biz-logic-js/app-2.png)
</div>
We can now test out that our select all / clear selection implementation work as it should in our multi select list.
## More Component Objects
In preparation for our upcoming script to implement the merge / copy / delete operations we need to save the selection state so we can easily access it in our Script. Again we will use a **Component Object**. For the actual storing operation of the selection we can use the node [Set Component Object Properties](/nodes/component-utilities/set-component-object-properties). Add in the node and add a property `Selection`. Set it to **Array** type.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/client-side-biz-logic-js/component-object-prop-1.png)
</div>
Then connect the outgoing **Selection** property from the `Multi Select List` component to the property. We should set it everytime the selection changes, so we also connect **Selection Changed** to **Do**.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/client-side-biz-logic-js/app-3.png)
</div>
## Using Function node for lightweight scripting
Our **Buttons** need a bit of business logic on their own. If you have no items selected the items should be disabled. And for the _merge_ operation to make sense you actually need at least two items selected.
If you want to write this in Javascript a **Script** node is a little heavy - we only are going to have one function. Instead we use the **Function** node.
So add in a **Function** node in our main app. Call it `Calculate Button States`. Also add three outputs called `DeleteEnabled`, `CopyEnabled` and `MergeEnabled`. They should be of **Boolean** type, because we want to connect them directly to the **Enabled** property of the **Buttons**.
<div className="ndl-image-with-background">
![](/docs/guides/business-logic/client-side-biz-logic-js/function-1.png)
</div>
The we open up the **Function** node and just write the code directly. In a **Function** node you don't have to write anything but your actual code, i.e. no function declarations etc is necessary.
Our code looks like below:
```javascript
Outputs.CopyEnabled = Component.Object.Selection !== undefined && Component.Object.Selection.length > 0;
Outputs.DeleteEnabled = Component.Object.Selection !== undefined && Component.Object.Selection.length > 0;
Outputs.MergeEnabled = Component.Object.Selection !== undefined && Component.Object.Selection.length > 1;
```
As you can see you access the outputs using `Outputs.<output name>` and inputs (if we had to use one) are accessed through `Inputs.<input name>`. You can also see that we make use of the **Component Object** again.
A **Function** node runs whenever an input changes, _unless the **Run** signal is connected in which case it only runs whtn **Run** is triggered_. In our case, we have no inputs, so we need to trigger **Run**. To make sure `Component.Object.Selection` is valid and set when we trigger it, we connect the outgoing **Done** from **Set Component Object Properties** to **Run**. We also hook up the outputs from the **Function** node to the **Buttons**.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/client-side-biz-logic-js/app-4.png)
</div>
Try out the logic and make sure the **Buttons** gets enabled/disabled as they should.
## Implementing the list operations
We are almost done. We just need to implement the actual operations in a **Script** node. Before that we need to think a little bit about how we treat the **Array** that holds the items of the list. Currently we take the items directly from our **Static Array**. This will not work once we start modifying them, we need to store the **Array** somewhere where the **Script** can retrieve it, and then update to a new **Array** once a copy / delete / merge occurs.
**Component Object** to the rescue! Create a **Component Object** node and add a property **Items**. Then connect the **Items** from the **Static Array** to it. Also connect from the **Component Object** to the `Mult Select List` component.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/client-side-biz-logic-js/app-5.png)
</div>
We are now ready to write our list operation scripts. Add a new **Script** node. We implement our three functions below:
```javascript
/* Returns a new array only continain the items that's selected */
function getSelectedItems () {
if (Component.Object.Selection === undefined) {
// if there is no selection, return an empty array
return [];
}
else {
return Component.Object.Selection.map ( (o) => Noodl.Object.get (o.Value));
}
}
/* returns a new array containing all items
except the ones that's selected */
function removeSelectedItems () {
return Component.Object.Items.filter ( (o) => {
// check if this item is in the selected list
if (Component.Object.Selection === undefined) {
// if there is no selection, the items should be kept in the list
return true;
}
else {
// check if this item is in the selected list. If it isn't return true (i.e. keep the item)
return Component.Object.Selection.find (p => (p.Value === o.id)) === undefined;
}
});
}
/*
Merge all selected items into one item.
Use the item with the latest date as the new item
delete the other items. */
Script.Signals.Merge = function () {
if (Component.Object.Items !== undefined && Component.Object.Selection !== undefined) {
// start with looking at the items that's selected
let itemsToMerge = getSelectedItems();
// sort them by latest date
let sortedItems = itemsToMerge.sort ( (o,p) => {
return new Date (p.delivery_date) - new Date (o.delivery_date);
});
// the first item in the sorted list is the one we want to keep
let itemToKeep = sortedItems[0];
// calculate the sum of all quantities in the items to merge
let sumOfQuantities = sortedItems.reduce ( (total , o) => total + o.quantity, 0);
// and store in the item we want to keep
itemToKeep.quantity = sumOfQuantities;
// remove all items that's selected
let itemsToKeep = removeSelectedItems ();
// Add back the item to keep
itemsToKeep.push (itemToKeep);
// this is our new items!
Component.Object.Items = itemsToKeep;
}
}
Script.Signals.Copy = function () {
if (Component.Object.Items !== undefined && Component.Object.Selection !== undefined) {
let selectedItems = getSelectedItems ();
// Get all selected items
// For each item in the selection, create a new object and copy the values
// Add a "-2" to the order_nbr
let copiedItems = selectedItems.map ( (o) => {
return Noodl.Object.create ({
order_nbr: o.order_nbr+"-2",
quantity: o.quantity,
delivery_date: o.delivery_date
});
});
Component.Object.Items = Component.Object.Items.concat (copiedItems);
}
}
Script.Signals.Delete = function () {
if (Component.Object.Items !== undefined && Component.Object.Selection !== undefined) {
Component.Object.Items = removeSelectedItems ();
}
}
```
Paste the code into the **Script** node. Again note that `Component.Object.Items` is always replaced with a new **Array** - we never modify the old one. This is to ensure that the multi select list understand that it needs to update.
Finally we connect our **Buttons** to the respective list operation and we are done!
<div className="ndl-image-with-background xl">
![](/docs/guides/business-logic/client-side-biz-logic-js/app-6.png)
</div>
You can import the full project below.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/client-side-biz-logic-js/final-1.png)
<ImportButton zip="/docs/guides/business-logic/client-side-biz-logic-js/biz-logic-js.zip" name="Multiselect list with js" thumb="/docs/guides/business-logic/client-side-biz-logic-js/final-1.png"/>
</div>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,189 @@
---
title: Custom UI Components
hide_title: true
---
import CopyToClipboardButton from '/src/components/copytoclipboardbutton'
import ImportButton from '../../../src/components/importbutton'
# Custom UI Components
## What you will learn in this guide
A very powerful feature of Noodl is the ability to create re-usable components easily. This guide will cover some useful patterns for create re-usable UI components. This guide will involve a bit of coding so it is good if you have some basic coding skills in Javascript and have read our previous guides on business logic in Javascript.
## Component Inputs and Outputs
The key to creating good re-usable components is to provide inputs and outputs that makes it useable. There are some good patterns to follow here and we will outline them here. We will start with a simple example where we create a component with a slider plus two labels. This is what it will look like:
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/custom-ui-components/slider-with-label.png)
</div>
And here is the content of the component. This is a simple UI component that has a slider and two text labels. One label is simple the header for the slider, and the other is formatted using the **String Format** node and the current **Value** and the **Max** value.
<div className="ndl-image-with-background xl">
<CopyToClipboardButton json={{"nodes":[{"id":"f73a5d7c-7b0e-e7f1-18a0-537f50623b8d","type":"Group","x":0,"y":0,"parameters":{},"ports":[],"children":[{"id":"5fb30353-669f-cee0-0339-e96608ad1478","type":"Text","x":20,"y":202,"parameters":{},"ports":[],"children":[]},{"id":"62063b48-b2c8-cf28-1de2-d20c7866ef42","type":"net.noodl.controls.range","x":20,"y":284,"parameters":{"marginBottom":{"value":15,"unit":"px"},"marginTop":{"value":15,"unit":"px"}},"ports":[],"children":[]},{"id":"fcee5809-211f-d59d-d566-7737e5383ceb","type":"Text","x":20,"y":406,"parameters":{"alignX":"right","sizeMode":"contentSize","color":"Light Gray"},"ports":[],"children":[]}]},{"id":"aaa82451-4b5d-874e-17c4-622b70e46249","type":"Component Inputs","x":-622.5,"y":167,"parameters":{},"ports":[{"name":"Label","plug":"output","type":{"name":"*"},"group":"Settings","index":0},{"name":"Value","plug":"output","type":{"name":"*"},"group":"Settings","index":1},{"name":"Max","plug":"output","type":{"name":"*"},"group":"Settings","index":2}],"children":[]},{"id":"1d8d66d2-b86d-6c90-a93c-8b3ffa7bfd56","type":"String Format","x":-184.5,"y":377,"parameters":{"format":"{Value} / {Max}"},"ports":[],"children":[]},{"id":"13725968-85f6-ee25-5e66-b7f208aac194","type":"Number","x":-387.5,"y":364,"parameters":{},"ports":[],"children":[]},{"id":"f4d370e6-ec69-5459-49e9-9d258172c77a","type":"Component Inputs","x":-227.5,"y":-20,"parameters":{},"ports":[{"name":"Margin Left","plug":"output","type":{"name":"*"},"index":0},{"name":"Margin Right","plug":"output","type":{"name":"*"},"index":1},{"name":"Margin Bottom","plug":"output","type":{"name":"*"},"index":3},{"name":"Margin Top","plug":"output","type":{"name":"*"},"index":2},{"name":"Align X","plug":"output","type":{"name":"*"},"index":4},{"name":"Align Y","plug":"output","type":{"name":"*"},"index":5},{"name":"Position","plug":"output","type":{"name":"*"},"index":6}],"children":[]},{"id":"51777154-9afa-4aa7-515a-6164a47ba35e","type":"Component Outputs","x":321.5,"y":287,"parameters":{},"ports":[{"name":"Value","plug":"input","type":{"name":"*"},"index":1},{"name":"Changed","plug":"input","type":{"name":"*"},"index":2}],"children":[]}],"connections":[{"fromId":"aaa82451-4b5d-874e-17c4-622b70e46249","fromProperty":"Label","toId":"5fb30353-669f-cee0-0339-e96608ad1478","toProperty":"text"},{"fromId":"aaa82451-4b5d-874e-17c4-622b70e46249","fromProperty":"Max","toId":"62063b48-b2c8-cf28-1de2-d20c7866ef42","toProperty":"max"},{"fromId":"62063b48-b2c8-cf28-1de2-d20c7866ef42","fromProperty":"value","toId":"1d8d66d2-b86d-6c90-a93c-8b3ffa7bfd56","toProperty":"Value"},{"fromId":"1d8d66d2-b86d-6c90-a93c-8b3ffa7bfd56","fromProperty":"formatted","toId":"fcee5809-211f-d59d-d566-7737e5383ceb","toProperty":"text"},{"fromId":"aaa82451-4b5d-874e-17c4-622b70e46249","fromProperty":"Max","toId":"13725968-85f6-ee25-5e66-b7f208aac194","toProperty":"value"},{"fromId":"13725968-85f6-ee25-5e66-b7f208aac194","fromProperty":"savedValue","toId":"1d8d66d2-b86d-6c90-a93c-8b3ffa7bfd56","toProperty":"Max"},{"fromId":"aaa82451-4b5d-874e-17c4-622b70e46249","fromProperty":"Value","toId":"62063b48-b2c8-cf28-1de2-d20c7866ef42","toProperty":"value"},{"fromId":"62063b48-b2c8-cf28-1de2-d20c7866ef42","fromProperty":"onChange","toId":"51777154-9afa-4aa7-515a-6164a47ba35e","toProperty":"Changed"},{"fromId":"62063b48-b2c8-cf28-1de2-d20c7866ef42","fromProperty":"value","toId":"51777154-9afa-4aa7-515a-6164a47ba35e","toProperty":"Value"},{"fromId":"f4d370e6-ec69-5459-49e9-9d258172c77a","fromProperty":"Align Y","toId":"f73a5d7c-7b0e-e7f1-18a0-537f50623b8d","toProperty":"alignY"},{"fromId":"f4d370e6-ec69-5459-49e9-9d258172c77a","fromProperty":"Align X","toId":"f73a5d7c-7b0e-e7f1-18a0-537f50623b8d","toProperty":"alignX"},{"fromId":"f4d370e6-ec69-5459-49e9-9d258172c77a","fromProperty":"Margin Top","toId":"f73a5d7c-7b0e-e7f1-18a0-537f50623b8d","toProperty":"marginTop"},{"fromId":"f4d370e6-ec69-5459-49e9-9d258172c77a","fromProperty":"Margin Bottom","toId":"f73a5d7c-7b0e-e7f1-18a0-537f50623b8d","toProperty":"marginBottom"},{"fromId":"f4d370e6-ec69-5459-49e9-9d258172c77a","fromProperty":"Margin Right","toId":"f73a5d7c-7b0e-e7f1-18a0-537f50623b8d","toProperty":"marginRight"},{"fromId":"f4d370e6-ec69-5459-49e9-9d258172c77a","fromProperty":"Margin Left","toId":"f73a5d7c-7b0e-e7f1-18a0-537f50623b8d","toProperty":"marginLeft"},{"fromId":"f4d370e6-ec69-5459-49e9-9d258172c77a","fromProperty":"Position","toId":"f73a5d7c-7b0e-e7f1-18a0-537f50623b8d","toProperty":"position"}],"comments":[]}} />
![](/docs/guides/business-logic/custom-ui-components/slider-with-label-nodes.png)
</div>
Let's take a closer look at the **Component Inputs** of this component. First we have a couple of inputs that are the basic settings for the component, the **Label**, **Max** and **Value** inputs. There are a couple of things to note about this component inputs. If you look at the **Max** input it is first connected to a **Number** node and then to the **String Format** node. This is a common pattern to ensure that the **Max** input is represented as a number in the property panel when this component is used. The component input will get the same type in the property panel, as the node it is connected to and in this case it is connected to both the **Max** of the **Slider** (which is a number) and the **Max** input of the **String Format** node which is a string. That fact that we go via a **Number** node will make sure the property panel knows what input field to show for that input.
<div className="ndl-image-with-background xl">
![](/docs/guides/business-logic/custom-ui-components/component-inputs-1.png)
</div>
Another thing to note is that the **Value** component input is connected to the **Value** input of the **Slider**. Most UI Components is collecting some sort of data from the user, in this case it's a range value, it is very important that the data is also exposed as an input so that it can be properly connected to a data source.
Moving on to the component outputs. Here you of course need the **Value** as an output as well, so that the UI component can be used to collect data from the user. It is also important to have a **Changed** signal.
<div className="ndl-image-with-background xl">
![](/docs/guides/business-logic/custom-ui-components/component-outputs.png)
</div>
:::warning
The **Changed** signal should **ALWAYS** be on a user input, not just if the **Value** input have changed. This is to make sure that the UI component doesn't report a change if the input value is changed. That can cause unnecessary data feedback loops.
:::
Finally it's a good idea to expose some minimum set of layout properties on the root node. This will make the UI component easier to use.
<div className="ndl-image-with-background xl">
![](/docs/guides/business-logic/custom-ui-components/component-inputs-2.png)
</div>
You can choose yourself what you want to expose as inputs but here are a few recommendations:
* **Margins** At least exposing margins will remove the need for extra **Group** nodes when using your component.
* **Align** It's also common to need to re-align your component when using it, providing this as an input is helpful.
* **Position** Maybe not as common, but could still be good to expose.
## Component Object
You have learnt how to use the [Object](/nodes/data/object/object-node) node in the [working with data guides](/docs/guides/data/objects) and how to connect it to UI controls in the [Connecting UI controls to data guide](/docs/guides/data/ui-controls-and-data). There is another node which is very useful when working on re-usable UI componets and that is the [Component Object](/nodes/component-utilities/component-object) node. This node works just like the **Object** node except that it is unique to the component instance, so it will not be shared between component instances like regular objects. This is very useful when keeping the state of UI controls.
We will take a look at a very simple example below, the **Segment Control** UI Component.
<div className="ndl-image-with-background l">
<ImportButton zip="/docs/guides/business-logic/custom-ui-components/segment-control-1.zip" name="Segment Control" thumb="/docs/guides/business-logic/custom-ui-components/segment-control.png"/>
![](/docs/guides/business-logic/custom-ui-components/segment-control.png)
</div>
This example actually contains two components the **Segment Control** component and the **Segment Control Button** component. What it does is that it accepts an array as input containing the possible options for the control, each object in the array should have a **Label** and a **Value**. It also accepts an input that is the current selected **Value** of the control, this should correspond to one of the values in the array and that button will be shown as selected (like radio buttons).
<div className="ndl-image-with-background xl">
![](/docs/guides/business-logic/custom-ui-components/segment-control-nodes.png)
</div>
Here you can see how we use the **Component Object** node to store the currently **Selected Value** and how it is also passed through as the **Value** output. We will take a look at how it is used later. The options input array is used directly as items in the **Repeater** node. If we take a closer look at the **Segment Control Button** component (that is used as template in the repeater) we will see where the magic happends.
<div className="ndl-image-with-background xl">
![](/docs/guides/business-logic/custom-ui-components/segment-control-button-nodes.png)
</div>
Let's go over this one quickly:
* The **Object** node is used to connect the **Label** to the button. So each button that is created by the repeater will get the correct label.
* Here we introduce a new action node, the [Set Parent Component Object Properties](/nodes/component-utilities/set-parent-component-object-properties) node that is used to set a property on the component object. But not the component object of this component instance, but instead it's closets visual parent. In this case (since this component is used as template in the repeater) it will be the **Segment Control** component. That is, each **Segment Control Button** component in the repeater will, when clicked, set it's **Value** as the **Selected Value** of the **Component Object**.
* Now we also use the [Parent Component Object](/nodes/component-utilities/parent-component-object) to compare the currently **Selected Value** with the **Value** of this segment control button, this is done in the **Function** node that takes both the currently selected value and the value from the repeater instance object as inputs. It has the following code:
```javascript
if(Inputs.SelectedValue == Inputs.MyValue)
Outputs.Variant = "Segment Control Button Selected"
else
Outputs.Variant = "Segment Control Button"
```
* Here comes the next little trick. The **Variant** of the **Button** is choosen by the **Function** to be either **Segment Control Button** or **Segment Control Button Selected**. We have created two different **Button** variants with those names so we can design how we want the button to look if it is selected and not. Learn more about style variants in [this guide](/docs/guides/user-interfaces/style-variants).
* Finally we send the **Click** signal from the button as **Component Output** from this component, this will allow us to use that signal from the **Repeater** node in the parent component.
The [Component Object](/nodes/component-utilities/component-object) and [Parent Component Object](/nodes/component-utilities/parent-component-object) nodes, and their action nodes to set properties, [Set Component Object Properties](/nodes/component-utilities/set-component-object-properties) and [Set Parent Component Object Properties](/nodes/component-utilities/set-parent-component-object-properties) are very useful when building re-usable UI components. We recommend storing the state of your UI component in these.
## State management
Some times you need to initialise your UI components when they are created. Then you can use the **Did mount** signal from the root UI element, often a **Group** node.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/custom-ui-components/did-mount.png)
</div>
You can also access the **Component Object** and **Parent Component Object** from **Function** and **Script** nodes by simply writing:
```javascript
Component.Object.MyValue = "Hello"
Component.Object["Selected Value"] = "Use this for properties with spaces"
Component.ParentObject.MyValue = "This works too"
```
So this is a great place to initialise your **Component Object** when the UI component is created.
Here is another interesting example to look at. This is a **Multi Checkbox Group** example. It takes two arrays as input, one with all possible options each with their
**Value** and **Label** and a second array which is the value, this array contains objects with just **Value**. So you can choose multiple options and not just one like the segment control. This is a little more complex so we wont go into detail here, but you can check out the example below and we will look at a few details.
<div className="ndl-image-with-background l">
<ImportButton zip="multi-checkbox.zip"/>
![](/docs/guides/business-logic/custom-ui-components/multi-checkbox.png)
</div>
If we take a close look at the nodes in the **Multi Checkbox Group** component, this is what we find:
<div className="ndl-image-with-background xl">
![](/docs/guides/business-logic/custom-ui-components/multi-checkbox-nodes.png)
</div>
Here is we can see that if any of the **Options** or **Selection** inputs change, we will run a **Function** node. The code of that node is as follows:
```javascript
if(!Inputs.Options) return // No options, no fun
Component.Object.Checkboxes = Inputs.Options.map(o => Noodl.Object.create({
id:Component.Object.id+'-'+o.Value,
Value:o.Value,
Label:o.Label || o.Value,
Checked:Inputs.Selection!==undefined && !!Inputs.Selection.find(s => s.Value === o.Value)
}))
```
It creates a new array of objects in the **Component Object** called **Checkboxes**, these will get the value and label, and a **Checked** property that is true if that value is represented in the in the selection. This array is that is then used in the **Repeater** node to show all components. It is important that this function is re-run if the **Options** or **Selection** is changed so that the UI control will always show the correct state as corresponds to its inputs.
:::note
We set the **id** of the object. This makes sure that the **Repeater** doesn't create new items every time the array changes. This increases performance.
:::
Another important thing to notice is that the **Selection** input is passed to the **Component Object** and then directly to the corresponding output. This is also very common.
Finally, we have another **Function** node that is run whenever the selection changes due to user input, just like the segment control component above this is sent out from the **Repeater** node. In this component we update the current **Selection** on the **Component Object** by filtering out the objects that currently are checked, and then filtering out the **Value** property for those objects. We update the **Component Object** which in turn will update the selection output.
```javascript
Component.Object.Selection = Component.Object.Checkboxes.filter(o => o.Checked).map(o => ({Value:o.Value}))
```
If we look at the **Multi Checkbox Group Item** component we will see that it is very basic. It is simply a checkbox that with the corresponding **Checked** and **Label** from the object in the **Checkboxes** array that we created before. When the checkbox is updated we update the **Checked** value of the object and report the change.
<div className="ndl-image-with-background xl">
![](/docs/guides/business-logic/custom-ui-components/multi-checkbox-item.png)
</div>
There you go, that's everything need to create a multi checkbox component. This pattern can be used to create all sorts of UI components with multi selection.

View File

@@ -0,0 +1,65 @@
---
title: Events
hide_title: true
---
# Events
## Overview
This guide covers an important concept in Noodl called Events. Events are used to send and recive signals with accompanying data. Signals are sent from one part of the node graph to one or many other locations in the graph. This is often useful when a user interaction occurs in one place of the app, such as the click of a button, that should trigger an action in a different place, e.g. a popup showing.
## What you will learn in this guide
This guide will teach you how to use the [Send Event](/nodes/events/send-event) and [Receive Event](/nodes/events/receive-event) nodes to pass signals and data from one place in your node graph to another.
This concept includes two nodes, the **Send Event** node and the **Receive Event** node. As the name implies, the **Send Event** node is used when you want to send an event. Below is an example of an event being sent when a **Text** node is clicked.
![](/docs/guides/business-logic/events/send-event.png ':class=img-size-l')
## Sending and receiving events
In the example above, the **Click** signal of the **Text** node is connected to the **Send** input of the **Send Event** node. This will trigger the an event to be sent when the text is clicked.
An event is sent to a certain **Channel** which is specified in the properties of the **Send Event** node. In this case the name of the channel is **Show Popup**.
![](/docs/guides/business-logic/events/channel-prop.png ':class=img-size-m')
The event signal is passed to all **Receive Event** nodes that share the same **Channel**. In the example below the event that was sent above is received.
![](/docs/guides/business-logic/events/receive-event.png ':class=img-size-l')
To illustrate this you can see below how when the click signal is sent via the **Send Event** node, it is passed to the **Received** output of the **Event Receiver** node.
![](/docs/guides/business-logic/events/events-demo.gif ':class=img-size-l')
## Passing payload data
So far we have seen the basic concept of the events mechanism in Noodl. Next, let's take a look at how you can pass data via payload connections to your event nodes. You start by adding ports to the **Send Event** node. You can add any number of ports for the data that you want to pass with the event.
![](/docs/guides/business-logic/events/add-port.gif ':class=img-size-l')
Now you can connect data to the input ports that you created on the **Send Event** node. When the **Send** signal is received, the values on all inputs of the **Send Event** node will be captured and passed to the **Receive Event**.
![](/docs/guides/business-logic/events/connect-to-port.png ':class=img-size-l')
When the **Receive Event** node outputs the **Received** signal it will also update all other outputs. The payload ports added on the **Send Event** node will become available on all **Receive Event** nodes that share the same channel as the **Send Event** node.
![](/docs/guides/business-logic/events/receiver-outputs.png ':class=img-size-l')
## Propagation
Event propagation means how an event is sent in the graph, i.e. which **Receive Event** nodes an event is sent to. The default propagation mode is **Global** which means _all_ receivers will be triggered. You can however change the propagation via the **Send To** property of the **Send Event** node.
![](/docs/guides/business-logic/events/send-to.png ':class=img-size-m')
The **Children** mode will send the events to all the children in the component where the **Send Event** node is. So in the example below, the event will first be sent to **My Child Comp** followed by any children that node may have. When all descendants of **My Child Comp** node have received the event it will pass it to all children that are dynamically created by the **Repeater** node, and their descendants.
![](/docs/guides/business-logic/events/send-to-children.png ':class=img-size-l')
The **Siblings** mode will pass the event to all other nodes that are on the same level as the node where the originating **Send Event** node is. So if for instance the **My Child Comp** in the graph below contains a **Send Event** node that sends an event to its siblings all other **My Child Comp** nodes will receive it, except for the one sending the event, followed by the child instances dynamically created by the **Repeater** node.
![](/docs/guides/business-logic/events/send-to-siblings.png ':class=img-size-l')
The last propagation mode is **Parent**. This mode will send events up the node graph hierarchy. The **My Other Child** in the example graph below contains a **Send Event** node that is using the **Parent** propagation mode. When an event is sent from **My Other Child**, the parent **My Child Comp** node with receive it, followed by the node we are in and then the event would be passed on to the parent of this node. The propagation follows the visual hierarchy chain.
![](/docs/guides/business-logic/events/send-to-parent.png ':class=img-size-l')
The final thing to know about propagation is the **Consume** property of the **Receive Event** node. If that property is checked it means that when that particular node receives an event it will stop the propagation. So no other **Receive Event** nodes after that one will receive this specific event.

View File

@@ -0,0 +1,969 @@
---
id: javascript
title: Javascript in Noodl
hide_title: true
---
import CopyToClipboardButton from "../../../src/components/copytoclipboardbutton";
# Javascript in Noodl
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/javascript/script-inline-code.png ":class=img-size-l")
</div>
## What you will learn in this guide
This guide will introduce you to how to use Javascript in Noodl.
While almost everything can be achieved in Noodl using Nodes,
if you know your way around Javascript it's sometimes just more convenient to add in Javascript code directly.
Noodl make the mix of nodes and code very easy using the [Function](/nodes/javascript/function) and [Script](/nodes/javascript/script) nodes.
### Overview
The guide will first go through the **Function** node,
which is excellent for simple, single function, Javascript code.
Then it will show a more extensive Javascript example using the **Script** node.
The two nodes can also be seen used combined in the [Business Logic using Javascript Guide](/docs/guides/business-logic/client-side-biz-logic-js)
## Using the **Function** node
The easiest way to write Javascript in Noodl is using the **Function** node.
There is literally no overhead at all - you just open the code editor and write code! Let's try it!
Create a new project using the "Hello World" template. Add in two [Text Input](/nodes/ui-controls/text-input) nodes before the text node.
Give them the labels "First String" and "Second String". Then add in a **Function** node. Call the function node "Merge Strings".
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/javascript/function-node-1.png)
</div>
Now click the **Function** node and edit the script. Paste in the Javascript below:
```javascript
if (Inputs.String1 !== undefined && Inputs.String2 !== undefined) {
let length = Math.max(Inputs.String1.length, Inputs.String2.length);
let outputString = "";
for (let i = 0; i < length; i++) {
if (i < Inputs.String1.length) {
outputString = outputString + Inputs.String1.charAt(i);
}
if (i < Inputs.String2.length) {
outputString = outputString + Inputs.String2.charAt(i);
}
}
Outputs.MergedString = outputString;
}
```
## Inputs and Outputs in the Function node
This little function merges two strings called `String1` and `String2`.
They are declared as `Inputs.String1` and `Inputs.String2` and will become inputs on the **Function** node.
You can also declare inputs manually on the function node - if you click it you will see that you can add inputs and outputs.
By adding them manually you can be a bit more precise on which type an input is,
but generally is not needed. One reason to explicitly state type of input is for example when you connect a **Function** node to a **Component Inputs**.
By knowing the type Noodl can present the right control in the property of the Component.
There is also an output defined in the code, `Outputs.MergedString`.
We can now connect the **Function** node.
Connect the **Text** property from the **Text Inputs** to `String1` and `String2` and connect the output `MergedString` to the **Text**.
Now if you start writing in the **Text Fields** you will see the two strings merged on the screen.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/javascript/function-node-2.png)
</div>
## Running the **Function** node on change or on a signal
There are two ways to make the **Function** run.
1. If **Run** signal is not connected, the **Function** will run as soon as any of its inputs are changed. This is the case with our current example.
2. If **Run** signal is connected, the **Function** will only run when triggered.
Let's try to change to number two. We only want to merge the string once the user clicks a button.
So add a [Button](/nodes/ui-controls/button) after the **Text Inputs**. Give it the label **Merge**.
Then connect **Click** to **Run** on the function.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/javascript/function-node-3.png)
</div>
## Sending Signals on Outputs
A **Function** node can also send signals. This is very useful for many scenarions,
for example to trigger something that should happen once the **Function** is executed.
For example, if we would want to store our merged string, we would want to trigger a [Set Variable](/nodes/data/variable/set-variable) node.
Let's add in the signal, once the merging is done:
Now click the **Function** node and edit the script. Paste in the Javascript below:
```javascript
if (Inputs.String1 !== undefined && Inputs.String2 !== undefined) {
let length = Math.max(Inputs.String1.length, Inputs.String2.length);
let outputString = "";
for (let i = 0; i < length; i++) {
if (i < Inputs.String1.length) {
outputString = outputString + Inputs.String1.charAt(i);
}
if (i < Inputs.String2.length) {
outputString = outputString + Inputs.String2.charAt(i);
}
}
Outputs.MergedString = outputString;
Outputs.Done();
}
```
Note the additional line `Outputs.Done()`. That's all you need to send a signal.
So add in a **Set Variable** node and save the value in an **Variable** called `Merged String`.
You might think that connecting directly from the **Button** to the **Do** action on the **Set Variable** might have worked, but it actually doesn't.
You cannot know if the **Function** node have executed so the **Do** signal may trigger at the wrong time.
Instead explicitly triggering `Done` once you've set the output to the correct value takes care of this.
Another common way of using outgoing signals could be to trigger two different paths going forward.
Perhaps the **Function** validates a data value and triggers a `Valid` signal if the value is ok,
that saves then triggers a database save, while a `Invalid` signal would open an error popup.
Now let's look at the more powerful **Script** node.
## JavaScript using the **Script** node
This part of the guide will cover the functionality of the [Script](/nodes/javascript/script) node.
The **Script** node is a great way to implement logic and calculations that are easier to express in code than in nodes and connections.
It's also a great way to get access to useful JavaScript APIs in the browser, for example `Date` for date functionality or `Math` for advanced math-functionality.
The **Script** node is a bit more powerful than the [Function](/nodes/javascript/function) node that also can be used to write JavaScript.
The **Script** node can have multiple methods and functions, as well as a well defined lifespan with callbacks when the node is created and destroyed.
For simpler JavaScript for processing inputs, usually the **Function** node is a simpler choice as you have seen above.
A **Script** node works as any other node in Noodl, in the sense that it has inputs and outputs that can be connected to other nodes.
All inputs and outputs are available to the developer in their JavaScript code.
In a **Script** node you can call any JavaScript function normally available in a browser environment.
## The Script source file
You can either edit the JavaScript code directly in the built-in editor in Noodl or you can use an external file with an external editor.
While it's easy to write code snippets in the inline editor, the external file option might be better if you are working on larger files or even multiple files and modules.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/javascript/script-inline-code.png ":class=img-size-l")
</div>
An external file needs to be located in your project folder for Noodl to find it. You can copy a file to your project folder by dragging the file onto the Noodl window.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/javascript/script-pick-file.png ":class=img-size-l")
</div>
The source code provided for the **Script** is executed as soon as the node is created. In order to specify inputs, outputs and receive and send signals from the node the `Node` object must be used.
## Inputs and outputs
There are a number of ways to specify the inputs and outputs of the **Script** node. One way is to use the property panel and explicitly add them there. You can also provide the type.
<div className="ndl-image-with-background">
![](/docs/guides/business-logic/javascript/function-3.png)
</div>
Another way is to specify them programmatically in the source code. The inputs are defined in the `Script.Inputs` object. Each input also specifies what type it is. The available types are:
- `number`
- `string`
- `boolean`
- `color`
- `object`
- `array`. This is for Noodl Arrays, not JavaScript arrays.
- `reference`. A reference to a Noodl node, accessible through the _This_ output of visual nodes.
- `cloudfile`
Note that there is no signal type for inputs, as the signals are handled by using the `Script.Signals` object (more on that later).
The outputs work in the same way as the inputs except that there's one more type you can use: `signal`. The signal type is used for triggering a pulse on an output rather than outputting a specific value. Below we have added outputs to a **Script** node.
Since the inputs and outputs are members of an object, they should be separated by a comma. Below is an example of a **Script** node with two inputs and one output.
```javascript
Script.Inputs = {
RangeLow: "number",
RangeHigh: "number",
};
Script.Outputs = {
RandomNumber: "number",
};
```
Lets use the two inputs `RangeLow` and `RangeHigh` to generate a random number on the `RandomNumber` output. To execute the code, we will introduce a signal, `Generate`.
## Input signals
Input signals are mapped to functions in the `Script.Signals` object in the JavaScript node. A signal function is called when the signal with the same name is triggered. Here's the implementation of the `Generate` signal. You can copy this code and add it to the **Script** source code.
```javascript
Script.Signals.Generate = function () {
let randomNumber =
Math.random() * (Script.Inputs.RangeHigh - Script.Inputs.RangeLow) +
Script.Inputs.RangeLow;
Script.Outputs.RandomNumber = randomNumber;
};
```
Let's connect the the inputs, outputs and signals to some nodes.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/javascript/random1.gif)
<CopyToClipboardButton json={{"nodes":[{"id":"20e7a891-e750-3d60-058f-804e324722eb","type":"Javascript2","x":-588.8750006232499,"y":331.12222156165456,"parameters":{"code":"Script.Inputs = {\n RangeLow: \"number\",\n RangeHigh: \"number\",\n}\n\nScript.Outputs = {\n RandomNumber: \"number\",\n}\n\nScript.Signals.Generate = function() {\n let randomNumber = Math.random() * (Script.Inputs.RangeHigh - Script.Inputs.RangeLow) + Script.Inputs.RangeLow;\n Script.Outputs.RandomNumber = randomNumber;\n}\n","RangeLow":0},"ports":[],"children":[]},{"id":"42ca61a1-d015-69dc-48ed-a13e76818194","type":"Group","x":-365.17777825429016,"y":268.1043044317845,"parameters":{"backgroundColor":"#FFFFFF","paddingTop":{"value":20,"unit":"px"},"paddingLeft":{"value":20,"unit":"px"},"paddingRight":{"value":20,"unit":"px"}},"ports":[],"children":[{"id":"a15e91b8-5f23-47ce-446c-fea6760a8058","type":"Group","x":-345.17777825429016,"y":314.1043044317845,"parameters":{"flexDirection":"row","sizeMode":"contentHeight","marginBottom":{"value":10,"unit":"px"}},"ports":[],"children":[{"id":"c2e35d8b-c32a-8d1b-754e-6d33a90e9a8f","type":"Text","label":"Click Text","x":-325.17777825429016,"y":360.1043044317845,"parameters":{"text":"Click here to generate number","fontSize":{"value":15,"unit":"px"},"alignY":"bottom","sizeMode":"contentSize"},"ports":[],"children":[]}]},{"id":"94d50d6a-1dc6-876f-39fb-2dfddf0749ef","type":"Group","label":"Arena","x":-345.17777825429016,"y":456.1043044317845,"parameters":{"borderStyle":"solid","borderWidth":{"value":2,"unit":"px"},"marginBottom":{"value":0,"unit":"px"},"flexDirection":"column","sizeMode":"explicit","height":{"value":35,"unit":"px"}},"ports":[],"children":[{"id":"7ccbd282-8a7a-1589-edac-acacf54cea24","type":"Circle","x":-325.17777825429016,"y":552.1043044317845,"parameters":{"size":30,"position":"absolute"},"ports":[],"children":[]}]}]},{"id":"60c3158a-64a2-7664-abb1-02dae2731e9d","type":"Expression","label":"arenawidth-circlewidth","x":-590.8015248634624,"y":443.9881840161512,"parameters":{"expression":"arenawidth-circlewidth"},"ports":[],"children":[]}],"connections":[{"fromId":"94d50d6a-1dc6-876f-39fb-2dfddf0749ef","fromProperty":"boundingWidth","toId":"60c3158a-64a2-7664-abb1-02dae2731e9d","toProperty":"arenawidth"},{"fromId":"7ccbd282-8a7a-1589-edac-acacf54cea24","fromProperty":"boundingWidth","toId":"60c3158a-64a2-7664-abb1-02dae2731e9d","toProperty":"circlewidth"},{"fromId":"60c3158a-64a2-7664-abb1-02dae2731e9d","fromProperty":"result","toId":"20e7a891-e750-3d60-058f-804e324722eb","toProperty":"RangeHigh"},{"fromId":"20e7a891-e750-3d60-058f-804e324722eb","fromProperty":"RandomNumber","toId":"7ccbd282-8a7a-1589-edac-acacf54cea24","toProperty":"transformX"},{"fromId":"c2e35d8b-c32a-8d1b-754e-6d33a90e9a8f","fromProperty":"onClick","toId":"20e7a891-e750-3d60-058f-804e324722eb","toProperty":"Generate"}],"comments":[]}}/>
</div>
## Reading inputs and setting outputs
You can read the inputs directly through the members of the `Script.Inputs` object, typically `Script.Inputs.AnInput`.
There are two ways to set the outputs. Set the value by setting the appropriate property of the `Script.Outputs` object:
```javascript
Script.Outputs.RandomNumber = randomNumber;
```
Set many outputs at the same time using the `Script.setOutputs` function:
```javascript
Script.setOutputs({
One: 1,
Two: 2,
});
```
This is useful when you have an object that contains multiple values you want to send at once.
Finally if you want to send a signal on an output you need to use the output as a function, calling it when you want to send the signal.
```javascript
Script.Outputs.MySignalOutput();
```
Now let's add a bit more code to our JavaScript example. Instead of the `Generate` signal we will implement `Start` and `Stop` signals and have the **JavaScript** node generate new numbers continuously. We will start a timer in `Start` that will trigger after a random time, influenced by the `Lambda` input. The higher the `Lambda` the shorter the time and the higher the rate of generated numbers.
?> See the <a href="https://en.wikipedia.org/wiki/Poisson_point_process" target="_blank">Poisson process</a> for the math behind generating a random number using a Poisson distribution.
When the timer is triggered, a random number is generated based on the ranges provided to the node. Finally a signal to notify that a new number has been generated is sent and the timer is restarted with a new timeout.
When the `Stop` signal is triggered the timer is stopped.
Here's the code that generates the random numbers with a Poisson distributed time in between them.
```javascript
Script.Inputs = {
Lambda: "number",
RangeLow: "number",
RangeHigh: "number",
};
Script.Outputs = {
Trigger: "signal",
RandomNumber: "number",
};
var timer;
function generateRandNum(rangeLow, rangeHigh) {
return Math.random() * (rangeHigh - rangeLow) + rangeLow;
}
function calculateIntervalMs(lambda) {
let interval = -Math.log(1.0 - Math.random()) / lambda;
// translate from seconds to milliseconds
return interval * 1000;
}
Script.Signals.Start = function () {
console.log("Start");
let timeOutFunction = () => {
// generate the random number
let randNum = generateRandNum(
Script.Inputs.RangeLow,
Script.Inputs.RangeHigh
);
// set it on the output "RandomNumber"
Script.setOutputs({ RandomNumber: randNum });
// Trigger the signal "Trigger"
Script.Outputs.Trigger();
// restart the timer at a new interval
timer = setTimeout(
timeOutFunction,
calculateIntervalMs(Script.Inputs.Lambda)
);
};
// start the first timer
let interval = calculateIntervalMs(Script.Inputs.Lambda);
timer = setTimeout(timeOutFunction, interval);
};
Script.Signals.Stop = function () {
clearTimeout(timer);
timer = undefined;
};
```
## Changed inputs
You can run code whenever an input is changed. In this particular case, when the `Lambda` input of the random number generator is changed, the timer interval should be updated to avoid waiting for the next timer to time out for the change to take effect. To handle a case like this, a function with the same name as the input, `Lambda`, is added in the `Script.Setters` object. An additional state variable, `started`, is added to make sure that changing the value when the timer is stopped won't cause it to start.
```javascript
var started = false;
Script.Setters.Lambda = function (value) {
if (started === true) {
clearTimeout(timer);
startTimer();
}
};
```
## The final code
After some small refactoring the final code looks as below:
```javascript
Script.Inputs = {
Lambda: "number",
RangeLow: "number",
RangeHigh: "number",
};
Script.Outputs = {
Trigger: "signal",
RandomNumber: "number",
};
var timer;
var started = false;
function generateRandNum(rangeLow, rangeHigh) {
return Math.random() * (rangeHigh - rangeLow) + rangeLow;
}
function calculateIntervalMs(lambda) {
let interval = -Math.log(1.0 - Math.random()) / lambda;
// translate from seconds to milliseconds
return interval * 1000;
}
function startTimer() {
let timeOutFunction = () => {
// generate the random number
let randNum = generateRandNum(
Script.Inputs.RangeLow,
Script.Inputs.RangeHigh
);
// set it on the output "RandomNumber"
Script.setOutputs({ RandomNumber: randNum });
// Trigger the signal "Trigger"
Script.Outputs.Trigger();
// restart the timer at a new interval
timer = setTimeout(
timeOutFunction,
calculateIntervalMs(Script.Inputs.Lambda)
);
};
// start the first timer
let interval = calculateIntervalMs(Script.Inputs.Lambda);
timer = setTimeout(timeOutFunction, interval);
}
Script.Signals = {
Start() {
started = true;
startTimer();
},
Stop() {
clearTimeout(timer);
started = false;
},
};
Script.Setters.Lambda = function (value) {
if (started === true) {
clearTimeout(timer);
startTimer();
}
};
```
## Using script nodes
Connecting to the the inputs and outputs, the **Script** nodes can be used as any other nodes in Noodl. As an example, the Random Generator **Script** node has been combined with a simple UI to control the inputs. The output of the random generator is used to move a circle on the screen and trigger state changes. We have also copy & pasted the **Script** node and use it two times. This works great, but remember that the JavaScript code is cloned if you are using an inline source so changing the code in one **Script** node does not affect the other. It's often a good idea to encapsulate a reusable **Script** node in a Noodl component.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/javascript/random3.gif)
<CopyToClipboardButton
json={{
nodes: [
{
id: "e1b76139-42b1-f1aa-d935-8697c02446e5",
type: "Group",
x: 550.8894402215635,
y: 769.5545779864274,
parameters: {
backgroundColor: "#FFFFFF",
paddingTop: { value: 20, unit: "px" },
paddingLeft: { value: 20, unit: "px" },
paddingRight: { value: 20, unit: "px" },
},
ports: [],
children: [
{
id: "7d179c30-3108-a71d-76bf-0f760b7f9417",
type: "Group",
x: 570.8894402215635,
y: 815.5545779864274,
parameters: {
flexDirection: "row",
sizeMode: "contentHeight",
marginBottom: { value: 10, unit: "px" },
},
ports: [],
children: [
{
id: "801c4a96-5219-b818-4564-87a704f84837",
type: "Text",
label: "Rate controller text",
x: 590.8894402215635,
y: 861.5545779864274,
parameters: {
text: "Random generator is",
fontSize: { value: 15, unit: "px" },
alignY: "bottom",
sizeMode: "contentSize",
},
ports: [],
children: [],
},
{
id: "f54f6144-306d-9339-4e9c-a1fd70d0910d",
type: "Group",
label: "On/Off Spinner",
x: 590.8894402215635,
y: 921.5545779864274,
parameters: {
sizeMode: "explicit",
height: { value: 20, unit: "px" },
clip: true,
alignY: "bottom",
marginLeft: { value: 10, unit: "px" },
flexDirection: "none",
width: { value: 30, unit: "px" },
},
ports: [],
children: [
{
id: "8d21ec9d-7d28-d1e1-6435-ed102c432236",
type: "Text",
label: "Off",
x: 610.8894402215635,
y: 1017.5545779864274,
parameters: {
text: "Off",
color: "#006394",
fontSize: { value: 15, unit: "px" },
sizeMode: "contentSize",
alignY: "bottom",
},
ports: [],
children: [],
},
{
id: "6ec39536-2ae0-13da-31a9-0c95693eb91a",
type: "Text",
label: "On",
x: 610.8894402215635,
y: 1113.5545779864274,
parameters: {
text: "On",
fontSize: { value: 15, unit: "px" },
color: "#006394",
sizeMode: "contentSize",
alignY: "bottom",
},
ports: [],
children: [],
},
],
},
],
},
{
id: "83c96b0c-c3b3-c331-4c9c-ea903740c149",
type: "Group",
label: "Text and Rate controller",
x: 570.8894402215635,
y: 1209.5545779864274,
parameters: {
flexDirection: "row",
sizeMode: "contentHeight",
marginBottom: { value: 10, unit: "px" },
},
ports: [],
children: [
{
id: "3e8aa8af-5940-099e-9aa4-04aafc48f785",
type: "Text",
label: "Set rate",
x: 590.8894402215635,
y: 1283.5545779864274,
parameters: {
text: "Set rate",
fontSize: { value: 15, unit: "px" },
alignY: "center",
sizeMode: "contentSize",
marginRight: { value: 20, unit: "px" },
},
ports: [],
children: [],
},
{
id: "e30dd54b-7feb-d649-f3f9-e8f89ed96f7e",
type: "Group",
label: "Rate Controller",
x: 590.8894402215635,
y: 1343.5545779864274,
parameters: {
sizeMode: "explicit",
height: { value: 30, unit: "px" },
flexDirection: "none",
width: { value: 200, unit: "px" },
},
ports: [],
children: [
{
id: "e6d5752f-cedd-429d-3d3d-173d747c8c85",
type: "Group",
label: "Rate controller Bg",
x: 610.8894402215635,
y: 1403.5545779864274,
parameters: {
height: { value: 20, unit: "px" },
backgroundColor: "#C6C6C6",
borderRadius: { value: 10, unit: "px" },
alignY: "center",
},
ports: [],
children: [],
},
{
id: "241b0a6f-b409-7cbd-dc61-3bc39c020667",
type: "Drag",
x: 610.8894402215635,
y: 1499.5545779864274,
parameters: {},
ports: [],
children: [
{
id: "31252340-8be5-166c-4c16-f3d178135139",
type: "Group",
label: "Drag handle",
x: 630.8894402215635,
y: 1581.5545779864274,
parameters: {
height: { value: 20, unit: "px" },
width: { value: 5, unit: "px" },
backgroundColor: "#434B53",
paddingLeft: { value: 0, unit: "px" },
marginLeft: { value: 10, unit: "px" },
marginRight: { value: 10, unit: "px" },
borderRadius: { value: 2, unit: "px" },
alignY: "center",
},
ports: [],
children: [],
},
],
},
],
},
],
},
{
id: "f227417b-8cd3-3c03-5501-1b85add9566c",
type: "Group",
label: "Arena",
x: 570.8894402215635,
y: 1641.5545779864274,
parameters: {
borderStyle: "solid",
borderWidth: { value: 2, unit: "px" },
marginBottom: { value: 10, unit: "px" },
flexDirection: "none",
},
ports: [],
children: [
{
id: "521f5c37-4508-f725-d67d-44f0227e3aa9",
type: "Circle",
x: 590.8894402215635,
y: 1737.5545779864274,
parameters: { size: 30, position: "absolute" },
ports: [],
children: [],
},
],
},
],
},
{
id: "238629f6-8594-ac78-ba25-a8141cebf4f6",
type: "States",
x: 110.74283186907337,
y: 946.5074351399836,
parameters: {
states: "Off,On",
values: "Off y position,On y Position",
"value-Off-Off y position": 0,
"value-On-Off y position": -20,
"value-Off-On y Position": 20,
"value-On-On y Position": 0,
},
ports: [],
children: [],
},
{
id: "606ead21-564f-f2d3-8311-30eb63af8eab",
type: "Javascript2",
label: "Poisson Process",
x: 360.9506961673602,
y: 1177.1836206411483,
parameters: {
code: 'Script.Inputs = {\n Lambda: "number",\n RangeLow: "number",\n RangeHigh: "number",\n}\n\nScript.Outputs = {\n Trigger: "signal",\n RandomNumber: "number",\n}\n\nvar timer;\nvar started = false;\n\nfunction generateRandNum(rangeLow, rangeHigh) {\n return Math.random() * (rangeHigh - rangeLow) + rangeLow;\n}\n\nfunction calculateIntervalMs(lambda) {\n let interval = -Math.log(1.0 - Math.random()) / lambda;\n // translate from seconds to milliseconds\n return interval * 1000;\n}\n\nfunction startTimer() {\n let timeOutFunction = () => {\n // generate the random number\n let randNum = generateRandNum(\n Script.Inputs.RangeLow,\n Script.Inputs.RangeHigh\n );\n // set it on the output "RandomNumber"\n Script.setOutputs({ RandomNumber: randNum });\n // Trigger the signal "Trigger"\n Script.Outputs.Trigger()\n // restart the timer at a new interval\n timer = setTimeout(\n timeOutFunction,\n calculateIntervalMs(Script.Inputs.Lambda)\n );\n };\n\n // start the first timer\n let interval = calculateIntervalMs(Script.Inputs.Lambda);\n\n timer = setTimeout(timeOutFunction, interval);\n}\n\nScript.Signals = {\n Start() {\n started = true;\n startTimer();\n },\n Stop() {\n clearTimeout(timer);\n started = false;\n }\n}\n\nScript.Setters.Lambda = function (value) {\n if (started === true) {\n clearTimeout(timer);\n startTimer();\n }\n}\n',
Lambda: 2,
RangeLow: 0,
},
ports: [],
children: [],
},
{
id: "cdd08f56-26b6-c81f-729b-ef7220190933",
type: "Expression",
label: "Lambda value",
x: 943.791823827788,
y: 1223.6044302506757,
parameters: { expression: "xpos/(DrageAreaWidth - 25)*3 + 0.1" },
ports: [],
children: [],
},
{
id: "26b6b95e-48c9-d4a5-2255-124bef51428b",
type: "Expression",
x: 297.4155454673813,
y: 1508.2775409338583,
parameters: { expression: "ArenaWidth - CircleRadius" },
ports: [],
children: [],
},
{
id: "99c3a02d-7b32-d31e-40d0-fc6bd485f2d3",
type: "Transition",
x: 272.28959152461647,
y: 1742.5301598853775,
parameters: {},
ports: [],
children: [],
},
{
id: "9a29693e-97b7-e24a-c39f-95745e15289a",
type: "Javascript2",
label: "Poisson Process",
x: 973.3810754787316,
y: 1711.8056993389462,
parameters: {
code: 'Script.Inputs = {\n Lambda: "number",\n RangeLow: "number",\n RangeHigh: "number",\n}\n\nScript.Outputs = {\n Trigger: "signal",\n RandomNumber: "number",\n}\n\nvar timer;\nvar started = false;\n\nfunction generateRandNum(rangeLow, rangeHigh) {\n return Math.random() * (rangeHigh - rangeLow) + rangeLow;\n}\n\nfunction calculateIntervalMs(lambda) {\n let interval = -Math.log(1.0 - Math.random()) / lambda;\n // translate from seconds to milliseconds\n return interval * 1000;\n}\n\nfunction startTimer() {\n let timeOutFunction = () => {\n // generate the random number\n let randNum = generateRandNum(\n Script.Inputs.RangeLow,\n Script.Inputs.RangeHigh\n );\n // set it on the output "RandomNumber"\n Script.setOutputs({ RandomNumber: randNum });\n // Trigger the signal "Trigger"\n Script.Outputs.Trigger()\n // restart the timer at a new interval\n timer = setTimeout(\n timeOutFunction,\n calculateIntervalMs(Script.Inputs.Lambda)\n );\n };\n\n // start the first timer\n let interval = calculateIntervalMs(Script.Inputs.Lambda);\n\n timer = setTimeout(timeOutFunction, interval);\n}\n\nScript.Signals = {\n Start() {\n started = true;\n startTimer();\n },\n Stop() {\n clearTimeout(timer);\n started = false;\n }\n}\n\nScript.Setters.Lambda = function (value) {\n if (started === true) {\n clearTimeout(timer);\n startTimer();\n }\n}\n',
Lambda: 2,
RangeLow: 0,
},
ports: [],
children: [],
},
{
id: "db634033-b137-02ea-5d50-706c06432557",
type: "Expression",
x: 952.9005306361687,
y: 1948.8030166078263,
parameters: { expression: "ArenaHeight-CircleRadius" },
ports: [],
children: [],
},
{
id: "0a89a18b-cfe3-c6f9-18bc-221a7ffb2182",
type: "Transition",
x: 910.411880595271,
y: 1523.391447982002,
parameters: {},
ports: [],
children: [],
},
{
id: "7249be82-0072-c1ab-1e4c-c0c60903f8cf",
type: "States",
label: "Circle Color",
x: 291.63095980838676,
y: 1874.4551715183388,
parameters: {
states: "Color 1,Color 2,Color 3,Color 4",
values: "Circle Color",
"type-Circle Color": "color",
"value-Color 1-Circle Color": "#A92952",
"value-Color 2-Circle Color": "#F0BF56",
"value-Color 3-Circle Color": "#006394",
"value-Color 4-Circle Color": "#5E4275",
},
ports: [],
children: [],
},
{
id: "31a54762-a486-533b-6041-e2b99c000ee4",
type: "States",
x: 659.6426944801517,
y: 2001.0264630214242,
parameters: {
states: "Small,Big",
values: "Radius",
"value-Small-Radius": 30,
"value-Big-Radius": 50,
},
ports: [],
children: [],
},
],
connections: [
{
fromId: "238629f6-8594-ac78-ba25-a8141cebf4f6",
fromProperty: "Off y position",
toId: "8d21ec9d-7d28-d1e1-6435-ed102c432236",
toProperty: "transformY",
},
{
fromId: "238629f6-8594-ac78-ba25-a8141cebf4f6",
fromProperty: "On y Position",
toId: "6ec39536-2ae0-13da-31a9-0c95693eb91a",
toProperty: "transformY",
},
{
fromId: "f54f6144-306d-9339-4e9c-a1fd70d0910d",
fromProperty: "onClick",
toId: "238629f6-8594-ac78-ba25-a8141cebf4f6",
toProperty: "toggle",
},
{
fromId: "241b0a6f-b409-7cbd-dc61-3bc39c020667",
fromProperty: "positionX",
toId: "cdd08f56-26b6-c81f-729b-ef7220190933",
toProperty: "xpos",
},
{
fromId: "e6d5752f-cedd-429d-3d3d-173d747c8c85",
fromProperty: "boundingWidth",
toId: "cdd08f56-26b6-c81f-729b-ef7220190933",
toProperty: "DrageAreaWidth",
},
{
fromId: "238629f6-8594-ac78-ba25-a8141cebf4f6",
fromProperty: "reached-On",
toId: "606ead21-564f-f2d3-8311-30eb63af8eab",
toProperty: "Start",
},
{
fromId: "238629f6-8594-ac78-ba25-a8141cebf4f6",
fromProperty: "reached-Off",
toId: "606ead21-564f-f2d3-8311-30eb63af8eab",
toProperty: "Stop",
},
{
fromId: "cdd08f56-26b6-c81f-729b-ef7220190933",
fromProperty: "result",
toId: "606ead21-564f-f2d3-8311-30eb63af8eab",
toProperty: "Lambda",
},
{
fromId: "521f5c37-4508-f725-d67d-44f0227e3aa9",
fromProperty: "boundingWidth",
toId: "26b6b95e-48c9-d4a5-2255-124bef51428b",
toProperty: "CircleRadius",
},
{
fromId: "26b6b95e-48c9-d4a5-2255-124bef51428b",
fromProperty: "result",
toId: "606ead21-564f-f2d3-8311-30eb63af8eab",
toProperty: "RangeHigh",
},
{
fromId: "606ead21-564f-f2d3-8311-30eb63af8eab",
fromProperty: "RandomNumber",
toId: "99c3a02d-7b32-d31e-40d0-fc6bd485f2d3",
toProperty: "targetValue",
},
{
fromId: "99c3a02d-7b32-d31e-40d0-fc6bd485f2d3",
fromProperty: "currentValue",
toId: "521f5c37-4508-f725-d67d-44f0227e3aa9",
toProperty: "transformX",
},
{
fromId: "cdd08f56-26b6-c81f-729b-ef7220190933",
fromProperty: "result",
toId: "9a29693e-97b7-e24a-c39f-95745e15289a",
toProperty: "Lambda",
},
{
fromId: "f227417b-8cd3-3c03-5501-1b85add9566c",
fromProperty: "boundingWidth",
toId: "26b6b95e-48c9-d4a5-2255-124bef51428b",
toProperty: "ArenaWidth",
},
{
fromId: "f227417b-8cd3-3c03-5501-1b85add9566c",
fromProperty: "boundingHeight",
toId: "db634033-b137-02ea-5d50-706c06432557",
toProperty: "ArenaHeight",
},
{
fromId: "521f5c37-4508-f725-d67d-44f0227e3aa9",
fromProperty: "boundingHeight",
toId: "db634033-b137-02ea-5d50-706c06432557",
toProperty: "CircleRadius",
},
{
fromId: "db634033-b137-02ea-5d50-706c06432557",
fromProperty: "result",
toId: "9a29693e-97b7-e24a-c39f-95745e15289a",
toProperty: "RangeHigh",
},
{
fromId: "9a29693e-97b7-e24a-c39f-95745e15289a",
fromProperty: "RandomNumber",
toId: "0a89a18b-cfe3-c6f9-18bc-221a7ffb2182",
toProperty: "targetValue",
},
{
fromId: "0a89a18b-cfe3-c6f9-18bc-221a7ffb2182",
fromProperty: "currentValue",
toId: "521f5c37-4508-f725-d67d-44f0227e3aa9",
toProperty: "transformY",
},
{
fromId: "238629f6-8594-ac78-ba25-a8141cebf4f6",
fromProperty: "reached-Off",
toId: "9a29693e-97b7-e24a-c39f-95745e15289a",
toProperty: "Stop",
},
{
fromId: "238629f6-8594-ac78-ba25-a8141cebf4f6",
fromProperty: "reached-On",
toId: "9a29693e-97b7-e24a-c39f-95745e15289a",
toProperty: "Start",
},
{
fromId: "7249be82-0072-c1ab-1e4c-c0c60903f8cf",
fromProperty: "Circle Color",
toId: "521f5c37-4508-f725-d67d-44f0227e3aa9",
toProperty: "fillColor",
},
{
fromId: "606ead21-564f-f2d3-8311-30eb63af8eab",
fromProperty: "Trigger",
toId: "7249be82-0072-c1ab-1e4c-c0c60903f8cf",
toProperty: "toggle",
},
{
fromId: "9a29693e-97b7-e24a-c39f-95745e15289a",
fromProperty: "Trigger",
toId: "31a54762-a486-533b-6041-e2b99c000ee4",
toProperty: "toggle",
},
{
fromId: "31a54762-a486-533b-6041-e2b99c000ee4",
fromProperty: "Radius",
toId: "521f5c37-4508-f725-d67d-44f0227e3aa9",
toProperty: "size",
},
],
comments: [],
}}
/>
</div>
## Debugging
As with any coding, you will sooner or later make a mistake in your code. Noodl will catch both syntax errors and runtime errors and highlight the **Script** node causing the error. You can also find errors in the warnings popup.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/javascript/script-error.png)
</div>
As seen in the image above, syntax errors in the code can cause inputs and outputs of the node to becomes invalid. Fixing the syntax error will restore the connections.
To debug your javascript you can launch the web debugger from the viewer window by clicking the bug icon.
<div className="ndl-image-with-background l">
![](/docs/guides/business-logic/javascript/open-debugger.png)
</div>
In the web debugger you can find any external source files that your are using in your script nodes, but if you want to set a breakpoint in an internal file you can use the `debugger` command. Here's an example:
```javascript
Script.Signals.Stop = function () {
debugger; // This will cause the debugger to break here when running
clearTimeout(timer);
timer = undefined;
};
```
### Running code when a Script node is created or destroyed
A Script node is created when the Noodl component that it belongs to is created. Components are created when the app is first launched, when navigation happens, and when a [Repeater](/nodes/ui-controls/repeater) node creates items. The `Script.OnInit` function is automatically called by Noodl when the Script node is created.
Components can be destroyed when doing navigation or when removing items from a list used by a Repeater node. This will run the `Script.OnDestroy` function.
Here's an example that sets up an event listener on the `body` element and removes it when the node is destroyed to avoid memory leaks.
```js
function setPosition(e) {
Script.Outputs.PointerX = e.clientX;
Script.Outputs.PointerY = e.clientY;
}
Script.OnInit = function () {
document.body.addEventListener("mousemove", setPosition);
document.body.addEventListener("mousedown", setPosition);
};
Script.OnDestroy = function () {
document.body.removeEventListener("mousedown", setPosition);
document.body.removeEventListener("mousemove", setPosition);
};
```

View File

@@ -0,0 +1,207 @@
---
title: Access Control
hide_title: true
---
# Access Control
## What you will learn in this guide
By now you should have a good understanding of how to create, update and query records in the database. If you feel the need to freshen up your skills, check out there guides.
- [Creating a Backend](/docs/guides/cloud-data/creating-a-backend)
- [Creating a Class](/docs/guides/cloud-data/creating-a-class)
- [Creating Records](/docs/guides/cloud-data/creating-new-database-records)
- [Record Relations](/docs/guides/cloud-data/record-relations)
By default all records you create are completely public, so they can be read and written by any user, logged in or anonymous. This is ok while you are developing your application but as soon as it is getting ready to be released outside of your team you will need to think about access control. That is who should be able to read and write which records. Fortunately there is a pretty solid way of achieving this in Noodl.
In this guide you will learn how to limit access control to records you create to certain users.
## Class Level Permissions
There are two levels you can specify access control to your cloud data, either **Class Level Permissions** that are the same for every record of a certain class, and the **Access Control Rules** which apply to a specific record only. Let's start with the former. You can access **Class Level Permissions** for a specific class via the cloud services dashboard.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/access-control/clp-1.png)
</div>
This will bring up the following popup.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-data/access-control/clp-2.png)
</div>
Here you can control the **Read**, **Write** and **Add Field** (the possibility to modify the properties for a class) permissions for a specific class. You can enable and disable them for:
* **Public** These are user of your application that is not signed in. So this means anyone, public, can perform these actions on records in this class. By default all operations, reading, writing and modyfing the fields are completely public.
* **Authenticated** This refers to users that have been signed up and logged in to your application. Again, this applies to all records of this class.
In the example below permissions are set so that everyone can read records in this class (this also include querying) but only logged in users can write (create new and modify) and no one (except when using the dashboard of course) can modify the properties of this class.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-data/access-control/clp-3.png)
</div>
You can specify a little bit more detailed access control using **Class Level Permissons** with roles and pointers. First just as you specified access for public vs authenticated users you can specify access for users belonging to a certain role (more on roles below). For that you use the text input at the bottom of the list:
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/access-control/clp-4.png)
</div>
You simply type `role:the-name-of-your-role` and then provide the permissions for that role. With this approach you could for instance have a role named `admin` and you could have special permissions for any users having that role. In the example below records of this class have public read access, but only users belonging to the role `admin` can modify records.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-data/access-control/clp-5.png)
</div>
You can use the `Simple / Advanced` switch at the top right to enable even more fine grained control. This will be very useful for some use cases. Now permissions are split up into:
* `Get` If you have an **Id** of a record, this permission allows the user to get the properties of the record.
* `Find` Records of this class will show up in a [Query Records](/nodes/data/cloud-data/query-records), this permission makes all records of this class readable.
* `Count` You can perform a count on this class, but directly read all records.
* `Create` Permission to create new records.
* `Update` Permission to update existing records.
* `Delete` Permission to delete existing records.
* `Add Field` Permission to modify the properties of this class, remove/add entire properties.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-data/access-control/clp-6.png)
</div>
Another way to specify more detailed access control is using pointers. So let's say you have a record class `Post` that contain blog posts. Now you want everyone to be able to read the records, but only the creator of the `Post` record should be able to modify it. If you add a pointer to a `User` record on your `Post` class (it must be a pointer to a `User`) you can specify permissions for the user that the pointer points to. Let's say you have a pointer on your class called `Owner` that you set to the current user when creating a post, you could then have an access control that looks like this.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-data/access-control/clp-7.png)
</div>
With **Class Level Permissions** you can specify access control down to a certain level, but all permissons are still on a class level which means they apply equally to all records of the class. Using **Access Control Rules** you can provide even more detailed permissions, read more about it below.
## Access Control Rules
You can set the access conrol of a record when it is created with the **Create New Record** node or, you can update it later usign the **Set Record Properties** node. This is done using the access control rules part in the property panel. Here is an example below.
<div className="ndl-image-with-background m">
![](/docs/guides/cloud-data/access-control/acl-1.png)
</div>
But when you place a new **Create New Record** node and view the properties the rules will be empty. You can create a new rule by clicking the **(+)** icon.
<div className="ndl-image-with-background m">
![](/docs/guides/cloud-data/access-control/acl-empty.png)
![](/docs/guides/cloud-data/access-control/acl-first.png)
</div>
You can have how every many access control rules you want and each rule has a specific **Target** that you need to pick:
- **User** (default) this indicates that this rule will target a specific user. You can provide the user by connection to the **User Id** input for the rule (this is only available if you have choosen the **User** target). Or if you don't explicitly provide a user id the current logged in user will be used.
- **Everyone** this implies that the rule will target all users, logged in or anonymous. This can be used to create public but read only records.
- **Role** this target should be used if you want this record to be accessible by a group of users. We will take a closer look at roles below.
First let's take a look at a common rule where we make sure the user creating the record has access rights to reading and writing the record.
<div className="ndl-image-with-background m">
![](/docs/guides/cloud-data/access-control/acl-creator.png)
</div>
This is simply the default settings for the rule. It is recommended that you edit the label of the rule and give it a short descriptive name so that you know later what it is supposed to achieve. Ok so if we use this rule to create records only the current user will be able to access them. This requires that a user has been [logged in](/nodes/data/user/log-in/).
Now let's say that we also want everyone to be able to read the records but not change them. Then we would add a rule with the **Everyone** target.
<div className="ndl-image-with-background m">
![](/docs/guides/cloud-data/access-control/acl-public-read.png)
</div>
There are many ways you can use this pattern to control who can access your record. Let's say you are creating a **Message** record and you want the current users (the sender) to be able to read and write the message, and the receiver to be able to read it. You would use a very similar set of rules but with two **User** targets.
<div className="ndl-image-with-background m">
![](/docs/guides/cloud-data/access-control/acl-msg-1.png)
</div>
But you will also need to provide the **User Id** of the receiver (the sender will user the current logged in user). This is done via a connection to the **User Id** input that is created when you add a new **User** target rule.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/access-control/acl-msg-2.png)
</div>
## Roles
This is great, but sometimes you want to have a record accessible by many users and if these users change over time it's a hazzle to update all records accordingly. This is where roles come in. A role is in it's essence simply a list of users (this is esstablished via a relation property called **users** on the role). You can add and remove users from the role using the **Add Record Relation** and **Remove Record Relation** nodes. You can add a role via the cloud services dashboard.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/access-control/acl-role-1.png)
![](/docs/guides/cloud-data/access-control/acl-role-2.png)
</div>
You need to provide a **Name** for your role (this needs to be unique among all roles), you also need to specify the **ACL** (the access control) and you should generally limit it to **Master Key Only** for roles that you create in the dashboard. Once the role is created you can add users to it via the **User** relation directly in the dashboard.
Once you have a role in place with users assiged to it you can simply create a **Role** access rule:
<div className="ndl-image-with-background m">
![](/docs/guides/cloud-data/access-control/acl-role-3.png)
</div>
You choose the **Role** target, give it a descriptive label and choose the access rights. Then you need to specify the **Role Name**, that is the unique name you gave the role when creating it. This is case sensitive.
Most cases you want to create and manage your roles dynamically, for instance if you want to create a team of users that should have access to certain records. Then you would create a role for that team and add relations to all team members.
This is achieved by creating a new role, which is done as any other record with the **Create New Record** node, picking the **Role** class. You need to limit the access control of the **Role** otherwise it will not succeed to create. Here we let the creater, i.e. the current user have full access and also we grant access to "everyone in the team". This is done via the role name, that is the same name as we give the role when creating it (we provide that via a connection as shown below).
<div className="ndl-image-with-background m">
![](/docs/guides/cloud-data/access-control/acl-create-role-1.png)
</div>
You need to provide a unique **name** for your new role, in the example below this is simply done with the **Unique Id** node. This is provided both to the **name** input for the role and also to the **Everyoone in Team** access rule as described above.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/access-control/acl-create-role-2.png)
</div>
Once the new **Role** is created we add the current logged in user to the role by adding a relation usign the **Add Record Relation** node. The current user already has read and write access to the role via **The creator** rule above, but we still add the user to the role as it should be listed as a team member. This is how we set up the **Add Record Relation** node.
<div className="ndl-image-with-background m">
![](/docs/guides/cloud-data/access-control/acl-create-role-3.png)
</div>
The **Id** is coming from the newly created role (that is the record where we want to add a relation), the class needs to be set to **Role** and the relation we want to add to is **users**.
Finally the **Target Record Id** is the user we want to add to the role and as you an see in the node graph above we get the from the **User** node that contains information on the currently logged in user.

View File

@@ -0,0 +1,118 @@
---
title: Creating a Cloud Service
hide_title: true
---
# Creating a Cloud Service for your project
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-data/creating-a-backend/dashboard-1.png)
</div>
## What you will learn in this guide
This guide will take you through the process of creating a **Cloud Service** for your Noodl project. A **Cloud Service** is needed if you want to fetch and store data for you application in a cloud **Database** and if you want to run and deploy **Cloud Functions**.
### Overview
We will go through the following steps in this guide:
- Creating a Cloud Services
- Selecting a Cloud Services for you project
- Inspect the **Cloud Services Database** using the **Dashboard**
- Create and switch to a new **Cloud Service**
## What is a Cloud Service and when do you need it?
A **Cloud Service** is a piece of software running in the cloud that helps serving your Noodl app with data and cloud functionality. The main reason for using a **Cloud Service** in your project is to use its **Database**. For example the nodes [Record](/nodes/data/cloud-data/record) and [Query Records](/nodes/data/cloud-data/query-records) can only be used if you have a **Cloud Service** active for you project. Also you need them for running [Cloud Functions](/docs/guides/cloud-logic/introduction).
Each **Cloud Service** has a dedicated database which means you might want to have multiple **Cloud Services** available for your project, for example one for your test data and one for your production data. Only one **Cloud Service** can be active for your project at one time, but when you [Deploy](/docs/guides/deploy/deploying-an-app-on-sandbox) you can choose which service to use for the deploy.
Also note that **Cloud Services** are shared within a workspace, i.e. all projects in a workspace have access to the all **Cloud Services**.
### Noold hosted vs Self Hosted Cloud Services
In Noodl there are two types of **Cloud Services**, **Noodl Hosted (default)** and **Self Hosted**. The **Noodl Hosted** are easiest to use, since Noodl can create those for you in a few clicks. **Self Hosted Cloud Services** are services that you set up and host yourself, check out [this guide](/docs/guides/deploy/using-an-external-backend), In this guide we will focus on **Noodl Hosted Cloud Services**.
To add a new Cloud Service click the **Cloud Services** tab icon in the side bar.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/creating-a-backend/cloud-services-tab.png)
</div>
Then click the **Plus** icon at the top of the sidebar.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/creating-a-backend/cloud-services-add.png)
</div>
Go to the **Add Cloud Service** tab.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-data/creating-a-backend/add-cloud-service.png)
</div>
Give the Cloud Service a name, for example "My Cloud Service" and a description "My cloud service for development". It could be a good idea to have multiple cloud services for the same project, since each cloud service will have its own database. This means that you can have one cloud service for development - where it doesnt matter if you mess up your data - and one for production.
## Select active cloud service
Now you can select the newly created cloud service as the active for your project. In the sidebar, click `Use in editor`.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/creating-a-backend/cloud-service-created.png)
</div>
This means that any data requests, for example from a [Query Records](/nodes/data/cloud-data/query-records), when running in the editor will use this cloud service.
## Inspect the Cloud Service using the Dashboard
Now your project is connected to a **Cloud Service** with a **Database**. The best way to get an overview of the database is to open the **Dashboard**. You open it by clicking the `Open dashboard` button on your Cloud Service.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-data/creating-a-backend/dashboard-1.png)
</div>
No you can see the contents of your **Database** in the menu to the left, under `Browser`. Right now, the Database contains two _classes_: `User` and `Role`. These two classes are created automatically. These classes are used to manage login and credentials in your app but we will not care about them in this guide.
### Classes
A **Class** in the database is a collection of **Records** of the same type. In other databases these are often referred to as **Tables** or **Collections**. In the case of the `User` class, it contains **User Records** where each record have data properties such as User Name, Email, etc. A typical app will have many different classes but we will not create any new classes in this guide. Instead you can close the **Dashboard** window for now.
## Creating and switching to a new cloud service
Now you can create a second cloud service, be following the same process as above. Give it a name, for example `Second cloud service` and a short description: `My production service`. You now have a second cloud service in the list, that you can make it the active backend for the editor by clicking `Use in editor`.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-data/creating-a-backend/second-backend.png)
</div>
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/creating-a-backend/second-backend-2.png)
</div>
Now you can switch back and forth between your two cloud services. But most often you use one for development and the other when you deploy.
## Deploying with a cloud service
When you deploy you application you pick which cloud service to use for the deploy. So you can for instance have one test deploy to sandbox that is using your development or testing cloud service, and another using your custom domain that use your production cloud service.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/creating-a-backend/deploy.png)
</div>

View File

@@ -0,0 +1,143 @@
---
title: Creating a Database Class
hide_title: true
---
# Creating a Class in the Database
## What you will learn in this guide
In this guide you will learn how to create new **Classes** in the database and add new **Columns** to it. **Classes** are used to hold **Records** in a Database in Noodl.
## Overview
We will go through the following steps in this guide
- Add a new **Class** to the Database using the **Dashboard**
- Add new **Columns** to the Class
- Add a couple of new **Records** in the Class through the **Dashboard**
Before you start this guide, make sure you have enabled Cloud Services and have one active for your project. You can learn how to do this in [this](/docs/guides/cloud-data/creating-a-backend) guide.
## Add a new Class to the database using the **Dashboard**
First, make sure you have an active cloud service for your project. Then click the `Dashboard` button in the Cloud Services sidebar.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-data/creating-a-class/dashboard-1.png)
</div>
Now you can add a new Class to the database either by clicking the yellow button `Create a Class` in the menu to the left, or open the `Edit` menu and select `Add a Class`.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/creating-a-class/create-class-1.png)
</div>
<div className="ndl-image-with-background m">
![](/docs/guides/cloud-data/creating-a-class/create-class-2.png)
</div>
Next step is to give the **Class** a name. Fill in a name, for example `Task` and make sure it's of the type `Custom`. Now you can select `Create class and add columns`.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/creating-a-class/add-class.png)
</div>
## Adding Columns to a Class
### What is a column?
Each **Class** in Noodl have a number of Columns which defines what type of data each **Record** in the class can have. For example, if you are building a Task Management app, you may have a class called `Task`, with the columns `task` and `isDone` as two columns, holding the description of the task and wether the task is done or not. In Noodl, the **Columns** will be represented as properties on the [Record](/nodes/data/cloud-data/record) node that can be used as inputs our outputs.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-data/creating-a-class/class.png)
</div>
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/creating-a-class/record-1.png)
</div>
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/creating-a-class/record-2.png)
</div>
:::note
[Relations](/docs/guides/cloud-data/record-relations?id=relation-many-to-many-relationships) are an exception here. They can only be used through Queries and won't shop up as properties on Record nodes.
:::
### Column Types
Each **Column** has a specific data type that decides what kind of data it can hold.
There are 11 different data types available:
- **String** - a string value
- **Boolean** - a boolean value (can be `true` or `false`)
- **Number** - a number value
- **Date** - a date (stored in iso format)
- **Object** - a JavaScript object
- **Array** - a JavasSript array
- **Geopoint** - A Geopoint, i.e. a location on the surface of the earth
- **Polygon** - a GEOJSon polygon object
- **File** - a reference to a file
- **Pointer** - A pointer to another **Record** in a **Class**
- **[Relations](/docs/guides/cloud-data/record-relations?id=relation-many-to-many-relationships)** - A list of relations to other **Records** in a **Class**. Note that these won't show up directly on the **Record** node, but are available in queries.
In this guide, we will focus in on the simpler data types, `String`, `Boolean` and `Number`.
Lets add two columns to our **Class**, `task` - a `String` type column, and `isDone` - a `boolean`. You can also chose to have a default value for each column that will be set if a new **Record** has no entry set for that column. In our case, let set the default value of `isDone` to `false`.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/creating-a-class/add-new-column.png)
</div>
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/creating-a-class/add-new-column-2.png)
</div>
You can also select if a **Column** is required for a new **Record** to be added in the **Class**. If set to `true`, the insertion will any new **Record** that has no value provided for the **Column**. Right now we don't want that, so we will leave it in the state `No` for both **Columns**.
### The default columns
As you can see when inspecting your new Task Class in the **Dashboard**, it also contains a few other **Columns**. These are automatically created you should generally not remove them. They are actually quite useful. The default columns are
- **objectId** - This is a unique identifyer for each **Record**. This identifyer will become the `id` property of the **Record** node when refering to it using a **Record** node.
- **createdAt** - A Date object containing the date and time for when the **Record** was created.
- **updatedAt** - A Date object containing the date and time for when the **Record** was last updated.
- **ACL** - A special column to keep track of which users and roles that has the right to fetch and store this object. For now, lets leave it as it is.
## Adding a new Record
Now it's time to add a new **Record** to the Task Class. You can add **Record** either through the `Add a row`-button or through the `Edit` menu and then selecting `Add row` or `Add row through modal`. Let's click on the latter button. This brings up a form that lets you fill out the values for each **Column** for the new **Record**.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/creating-a-class/add-row-1.png)
</div>
<div className="ndl-image-with-background s">
![](/docs/guides/cloud-data/creating-a-class/add-row-2.png)
</div>
Add a few **Records** and see them pop up in the **Class**. You can also try deleting them by selecting them and click `Edit`->`Delete these rows`.

View File

@@ -0,0 +1,128 @@
---
title: Creating new Database Records
hide_title: true
---
# Storing and fetching data in a database
## What you will learn in this guide
In this guide you will learn how to create and insert new entries - **Records** in a cloud database using the
[Create New Record](/nodes/data/cloud-data/create-new-record) node.
## Overview
We will go through the following steps in this guide:
- Create a **Record** in the database
- Retrieving the **Id** of the newly created **Record**
- Setting initial values of the **Record**
Before starting this guide, make sure you have Cloud Services enabled and have at least one **Class** in the database. You can learn how to do this in [this](/docs/guides/cloud-data/creating-a-backend) and [this](/docs/guides/cloud-data/creating-a-class) guide.
## Create a Record using the Create New Record Node
If you tried out the **Dashboard** you could see how you could create **Records** from there. Now we will learn how to create **Records** from within an app.
Start a new Noodl Project in your workspace. You can use any template, for example the `Hello World` template.
Again, make sure you have **Cloud Services** enabled and an active **Cloud Service** set up with at least one **Class**. When opening your `Cloud Services` sidebar it should look something like the image below, with a `Used in editor` cloud service selected.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/creating-new-database-records/cloud-service-active.png)
</div>
Now open the Node Picker by right clicking in the node graph editor panel. Create a **Create New Record** node.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/creating-new-database-records/node-picker-1.png)
![](/docs/guides/cloud-data/creating-new-database-records/create-new-record-1.png)
</div>
### The Create New Record Node
As it name states, this node is used to create and insert new **Records** in a **Class** in the Database. First we need to set up which **Class** it should insert **Records** into. Click the node and select one of you **Classes** that you set up in your Backend. In this example, we select the **Class** **Task**.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/creating-new-database-records/create-new-record-2.png)
</div>
### Initial Values
As you can see, when selecting a **Class** for your **Create New Records** node you immedieatly get new Properties on your node, namely the **Columns** you added to your **Class** when creating it. The `Task` class created in the previous guides have the `task` and the `isComplete` property.
You can add or remove **Columns** at any time in the **Dashboard** and all nodes that references to that **Class** will be updated accordingly. If you remove a **Column** that's used by your nodes, you will get warnings in Noodl.
As you probably already have figured out, by setting these properties on the node, you can control the initial values of the **Record** when creating it. So let's fill out the properties with some values, for example as below.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/creating-new-database-records/create-new-record-3.png)
</div>
### Creating The Record
Actually creating the **Record** is easy. You need to trigger the **Do** input signal on the **Create New Record** node. So lets add a [Button](/nodes/ui-controls/button) for that purpose. Add it somewhere on your screen. In this example we add it after the **Text** node and center it and change its label.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/creating-new-database-records/app1.png)
</div>
Now connect the **Click** output signal from the **Button** to the **Do** signal of the **Create New Record** by dragging from the **Button** to the **Create New Record** node.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/creating-new-database-records/connecting-1.png)
</div>
When the two nodes are connected you are ready to try your app. Click the button in the app a couple of times. You should see the **Clicked** to **Do** connection light up. If everything works as expected, a new **Record** should be created in the database each time you click. You can double check it by opening the **Dashboard** and look in your **Class**. Note: You may have to click the **Refresh** button in the **Dashboard** to see the items.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-data/creating-new-database-records/create-record.gif)
![](/docs/guides/cloud-data/creating-new-database-records/dashboard-1.png)
</div>
## Retrieving the id of the newly created Record
As you can see, the **Create New Record** node is kind of a **Record** creating factory. Each time **Do** is triggered a new **Record** is created. So how do you keep track of them?
The node has an output **Id**, which will be the identifyer for the **Record** just created. As a side note, assuming the creation went well, the **Success** output signal is triggered as well when the **Record** has been created. If the creation failed, for example if one of the properties of the **Record** is required and wasn't provided (see the [this](/docs/guides/cloud-data/creating-a-class/) guide), the **Failure** signal is triggered instead.
Lets connect the output **Id** of the **Create New Record** node to a **String** node to take a better look at it.
The **Id** is a special property of the **Record**. You will first notice that the **Id** of the newly created **Record** matches the **objectId** in the **Dashboard**.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-data/creating-new-database-records/inspect-id.png)
![](/docs/guides/cloud-data/creating-new-database-records/inspect-2.png)
</div>
The **Id** is used to refer to this specific **Record**. For example, if you want to change a property of a specific **Record** you use a [Set Record Properties](/nodes/data/cloud-data/set-record-properties) node and make sure it's **Id** is referring to the correct **Record**. Same goes for the [Record](/nodes/data/cloud-data/record) node or the [Delete Record](/nodes/data/cloud-data/delete-record). So keeping track of you **Ids** is important.
## Setting initial values of a Record
Now lets add some UI so each new Task **Record** can have a different task text, and can have `isDone` set to `true` or `false`. Add a [Text Input](/nodes/ui-controls/text-input) and a [Checkbox](/nodes/ui-controls/checkbox/) and connect them to the `Create New Record` node. As you can see, the two properties coming from the **Columns** of the **Class** are available as inputs on the **Create New Record** node.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-data/creating-new-database-records/initial-props.png)
</div>
Try out your app, write some random task descriptions and check / uncheck. See that it works correctly by looking at the data in your **Dashboard**.

View File

@@ -0,0 +1,171 @@
---
title: Filtering Database Queries
hide_title: true
---
# Filtering Database Queries
## What you will learn in this guide
In this guide you will learn how to let your database make a filtered query of your **Records**. This is a very common task in apps. For example if you want to see all comments related only to one specific post, or all messages that are unread. This is achieved by querying the database and ask it to filter out only the selected results.
In Noodl you use the [Query Records](/nodes/data/cloud-data/query-records) node to make a filtered query.
## Overview
We will go through the following steps in this guide
- Query the Database for **Records** with a property equal to a specified value
- Make a dynamic filter, i.e. where the Qurey filter can change dynamically
To get most out of this guide, it's best that you are already familiar with how to set up a cloud backend, create **Classes** and **Records** and make basic queries. You can quickly learn that by going through the following guides:
- [Creating a Backend](/docs/guides/cloud-data/creating-a-backend)
- [Creating a Class](/docs/guides/cloud-data/creating-a-class)
- [Creating Records](/docs/guides/cloud-data/creating-new-database-records)
- [Query Records](/docs/guides/cloud-data/quering-records-from-database)
- [Update Records](/docs/guides/cloud-data/updating-records)
## Filtering Records in the Cloud vs Locally
This guide is focusing in on filtering Queries in the Cloud Database. This means that **Records** and filtered before they are sent to your app over the network. This is what the [Query Records](/nodes/data/cloud-data/query-records) node does.
There is also another node, [Filter Records](/nodes/data/cloud-data/filter-records), that filters data _that is already in the app_, i.e. it filters locally.
Both of these have their advantages and disadvantages and in a good app you often mix them.
Filtering in a Query in the Database (using **Query Records**) have the following advantages
- You only send the filtered out **Records** over the network. This is incredibly important if you are working with large data sets. If you have thousands of products in a database, you only want to send the products that the user is searching for or your app will be slow.
- You can make use of optimized indexes in the Database if you for example are sorting or filtering out only certain objects. Again, if you work with large data sets with thousands or millions of **Records** this is key to make your app fast.
Filtering a Query locally (using **Filter Records**) have these main advantages
- Once the **Records** are in the app, you don't need to send **Records** over the network which makes your app much faster.
- If you have many users of your app your Backend and Database may be congested. By avoiding Querying the Cloud Database too often, for example by handling data locally, you put less stress on it.
Often, the most optimal solution is to combine the two methods. Make a Filtered Query towards the database that filters down the amount of **Records** to be sent to the app to a reasonable number, then use **Filter Records** for additional filtering and sorting locally.
In this guide, we will specifically look at filtering using the **Query Records** node. The **Filter Records** node works in a very similar fashion but only works on **Arrays** of **Records** that are already in the App, typically coming from a **Query Records** node.
## Using the Query Records node for filtering
This guide assumes that you already have a Backend up and running, with at least one Class containing a number of **Records**. You can follow the previous "Working With Cloud Data" guides if you need help with that. As an example we will use a simple Task app created in the previous guides. It has one **Class** called `Task`. It has two properties, `task` which is a description of the task, and `isDone` a boolean that keeps track of if the task is completed or not.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/filtering-database-queries/checkbox-anim.gif)
</div>
The App consists of a main screen as below:
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/filtering-database-queries/orig-app.png)
</div>
Each todo **Record** is represented by a list item constructed as below:
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/filtering-database-queries/list-item-2.png)
</div>
Now we want to add a filter to only see the uncompleted tasks. Click on the **Query Records** node in the main screen. Then click the `Add Filter Rule` button.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/filtering-database-queries/add-filter-rule-1.png)
</div>
A new popup will appear where you can construct your filter. Feel free to play around a little with it.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/filtering-database-queries/add-filter-rule-2.png)
</div>
It basically has three parts:
`<property> <operator> <value or input>`
The `<property>` is a property of your **Record**, for example `isDone` in our Task example.
The `<operator>` is a logical operator for the condition. There are a number of different operators and not all operators are available for all types of properties.
For the case of `isDone` - a **boolean** there are for operators available: `equal to`, `not equal to`, `exists` and `not exists`. While the `equal to` / `not equal to` are pretty self explanatory, the `exists` / `not exists` operators work in the following way: They check whether there is a value set at all for the property, or if it's undefined.
Finally the `<value or input>` is the value that the operator should be applied to. The `<input>` option we will look at later, so let's use `<value>` for now.
In our case we want to filter out only the tasks that are not yet completed, i.e with `isDone = false`. So our filter will be:
`<isDone> <equal to> <false>`
Let's select that. You can see that the somewhat cryptic format of the filter is spelled out in natural language below the filter.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/filtering-database-queries/add-filter-rule-3.png)
</div>
You should already now see the list in your app changing to only show tasks that are uncompleted (if you have any). If you check the tasks they will start disappearing one by one as they are being filtered out. If you want them back, you will have to go into the **Dashboard** and change the `isDone` value to false again, and refresh your app.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/filtering-database-queries/completing-tasks.gif)
</div>
## Dynamic filtering
The current state of the app is obviously flawed - when you finish all your tasks you see nothing. We need to be able to switch views between the completed and the uncompleted tasks.
Lets add a [Radio Button Group](/nodes/ui-controls/radio-button-group) with two [Radio Buttons](/nodes/ui-controls/radio-button). With this, we can control wether we want to show the uncomplete tasks or the completed tasks.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/filtering-database-queries/radiobutton-1.png)
</div>
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/filtering-database-queries/radiobutton-2.png)
</div>
Make sure the label for the buttons are set correctly ("Show Uncompleted"/"Show Completed") and that their value is set to "Uncompleted"/"Completed" respectively. Also, the **Value** of the **Radio Button Group** should be "Uncompleted". That will be the default state. We will use the **Value** output of the **Radio Button Group** to control the filter settings of the **Query Records**.
Now lets update the **Query Records** node so we can control its filter using inputs. Click the **Query Records** node and change the last part of the filter to not take a value, but an **input**.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/filtering-database-queries/add-filter-rule-4.png)
</div>
Name the input "isDoneFilter". Now the **Query Records** have a new input that we can use! If the input is set to `true` we will filter out all completed tasks (`isDone = true`) and vice versa for `false`.
Finally we need to convert the two Radio Button values "Uncompleted" and "Completed" to `true` or `false`. We do that by creating an [Expression](/nodes/math/expression) node and setting the expression to
`filterState === "Completed"`
The **Expression** node will output `true` if the input is "Completed", otherwise `false`. Finally connect the output of the **Expression** to the **Query Records** and - voila! - our filtering will change when clicking the radio buttons.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/filtering-database-queries/radiobutton-3.png)
</div>
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/filtering-database-queries/radiobutton-ui.gif)
</div>

View File

@@ -0,0 +1,109 @@
# Importing and exporting data with CSV
You can import and export data to/from your cloud service in the CSV (Comma Separated Values) format. This is done in the cloud services dashboard.
To export, browse the data for a specific record class and then you find the export commands in the toolbar at the top.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/import-export-csv/export-csv.png)
</div>
You can either export all records in a class (max 10K) or only the selected records. Select an option and then save the exported CSV.
To import a CSV you must also browse to the record class that you want to import into. Then you will find the import command in the `Edit` menu at the very bottom.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/import-export-csv/import-csv.png)
</div>
Click and then pick the CSV file on your computer that you want to use for import. The CSV file must follow a certain format for the import to work well.
### String
The input will be assigned directly as a string.
### Boolean
When the column data type is a boolean, there are a few different formats that are possible:
```js
"True" // == true
"true" // == true
"1" // == true
1 // == true
"FalsE" // == false
"false" // == false
"0" // == false
0 // == false
```
### Number
When the column data type is a number, it will convert the input to a number and assign if the value is not invalid or NaN.
### Date
Everything that is available via `new Date(...)` will be possible to import.
For example:
```json
"Thu Apr 13 2023 10:07:29 GMT+0200 (Central European Summer Time)"
"2023-04-13T08:02:46.447Z"
"Thu, 13 Apr 2023 08:06:11 GMT"
```
### Object
You can import an object type property by specifying it as JSON:
```json
{
"some_property":10
}
```
### Array
Properties of array types can also be provided as JSON.
```json
[1,2,3]
```
### GeoPoint
A propety of type GeoPoint can be imported either as a string with latitude and longitude:
```json
"30,30"
```
Or as an object with lat/long as properties:
```json
{"latitude": 30, "longitude": 30}
```
### Pointer
A point is simply a string with the `Id` of the target record.
### Relation
This is currently not supported.
### File
```json
{
"url": "path-to-file",
"name: "filename.png"
}
```

View File

@@ -0,0 +1,13 @@
---
title: Working with Cloud Data in Noodl
hide_title: true
---
import ReactPlayer from 'react-player'
# Working with Cloud Data in Noodl
Almost all real Apps needs a database. Noodl provides a professional grade database as part of every project.
<ReactPlayer playing autoplay muted loop url='overview/db3.mp4' />
### [Learn about Cloud Data in Noodl](/docs/guides/cloud-data/creating-a-backend)

View File

@@ -0,0 +1,170 @@
---
title: Querying records From Database
hide_title: true
---
import CopyToClipboardButton from '../../../src/components/copytoclipboardbutton';
# Querying records from database
## What you will learn in this guide
In this guide you will learn how to use the [Query Records](/nodes/data/cloud-data/query-records) node to create powerful database queries to retrieve **Records** from a database. You will also learn how to use the [Repeater](/nodes/ui-controls/repeater) node in combination with a [Record](/nodes/data/cloud-data/record) node to visualize the results of a query.
## Overview
We will go through the following steps in this guide
- Create a simple query
- List results as list items using a **Repeater** node
- Connecting data properties to visual nodes
To get most out of this guide, it's best that you are already familiar with how to set up a cloud backend, create **Classes** and **Records**. You can quickly learn that by going through the following guides:
- [Creating a Backend](/docs/guides/cloud-data/creating-a-backend)
- [Creating a Class](/docs/guides/cloud-data/creating-a-class)
- [Creating Records](/docs/guides/cloud-data/creating-new-database-records)
## What is a "Query"?
In the database world a Query is how you ask a database for a specific subset of records based on some conditions. In the case of Noodl, you ask your **Cloud Service** for all **Records** in a **Class** that fullfills one or more conditions. An example of a query, in words, would be "Give me all **Records** in the **Class** `Task` where the property `isDone` is false".
## Making a Query
First make sure you have started a project with an active Backend with some data available in one or more **Classes**. Following the previous cloud data guides, we will use a **Class** called `Task`, with a bunch of items with the properties `task` - a description of a task, and `isDone` - a boolean value that is either `true` (the task ha been completed) or `false` (the task is not completed yet).
Lets start with an empty project. You can for example use the "Hello World" template and remove the text node. Add a [Repeater](/nodes/ui-controls/repeater/) node as a child to the main **Group**. We will need the **Repeater** later to visualize the results of our query.
Then create a [Query Records](/nodes/data/cloud-data/query-records) node.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/quering-records-from-database/query-1.png)
</div>
First we need to select which **Class** this **Query Records** node should be applied to. You do that by clicking the node and select a **Class** from the Class dropdown. You should expect to see all **Classes** you created in this dropdown. If you for some reason don't see them, you probably havent connected your project to the right cloud service.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/quering-records-from-database/query-2.png)
</div>
Leave the other settings for now. Since we don't add any conditions ("filters" as they are called in Noodl) this query will return all **Records** in the `Task` Class. Or actually not always all, since Noodl will have a default limit on 100 **Records** for a Query, to avoid flooding the network if the **Class** contains a large amount of items. You can change the limit, by checking the `Use Limit` checkbox, but we will not touch that right now.
To get a first look at we get back from the Query, create an [Array](/nodes/data/array/array-node) node to store the results in. Then connect **Items** from the **Query Records** node to the **Items** input on the **Array**. You should immediately see the result of the query if you hover over the connection between the two nodes to see the debug info.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-data/quering-records-from-database/query-running.png)
</div>
Noodl automatically runs **Query Records** as soon as they are created _unless_ the input signal **Do** is connected. Since queries and the result of queries are sent over network its sometimes important to control exactly when the query should be performed. In those cases connecting the **Do** signal to control when the query is performed is important. However, in this simple example we don't care about that so leaving it unconnected is fine.
## Visualising the query results using a Repeater node
To visualize the results we need to do a little bit of Noodling. We need to create a simple list item that can represent each **Record**.
Create a new visual component by clicking the "+" icon under components and then selecting `Visual Component`. Give the new component a good name, we will call our item "Task Item" as it will represent one task.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/quering-records-from-database/new-component.png)
</div>
In the visual component add two nodes under the existing **Group** node, one [Checkbox](/nodes/ui-controls/checkbox) and one [Text](/nodes/basic-elements/text) node. The **Checkbox** will represent the `isDone` property, while the **Text** node holds the description of the task. Also clean up the layout by setting the **layout** of the **Group** to **Horizontal** and add som padding and margins. Also the label of the **Checkbox** was removed. (You can copy the nodes using the button on the image below and paste them into your project if you want to.)
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-data/quering-records-from-database/task-item.png)
<CopyToClipboardButton
json={{"nodes":[{"id":"d4117337-b6cf-64cc-e2dd-70de4cfb10e6","type":"Group","x":0,"y":0,"parameters":{"flexDirection":"row","sizeMode":"contentHeight","paddingLeft":{"value":5,"unit":"px"},"paddingTop":{"value":2,"unit":"px"},"paddingBottom":{"value":2,"unit":"px"},"paddingRight":{"value":5,"unit":"px"}},"ports":[],"children":[{"id":"e5bdf35a-988f-9c6b-981e-0275f1884912","type":"net.noodl.controls.checkbox","x":20,"y":46,"parameters":{"useLabel":false,"alignY":"center"},"ports":[],"children":[]},{"id":"5fbfaf9d-919f-699f-1e1a-fcbdd20c7631","type":"Text","x":20,"y":92,"parameters":{"marginLeft":{"value":10,"unit":"px"},"sizeMode":"contentSize"},"ports":[],"children":[]}]}],"connections":[],"comments":[]}}
/>
</div>
The looks don't matter right now and if you work with different kind of data your list item will look a little different.
Next we go back to our main App and set the **Template** of the **Repeater** node to use our newly created Task Item.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/quering-records-from-database/repeater-template.png)
</div>
Finally, we delete the **Array** node we created earlier and feed the result of the query results directly into the **Repeater** node by connecting **Items** in the **Query Records** node to the **Items** input in the **Repeater**.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-data/quering-records-from-database/query-to-repeater.png)
</div>
After the connection is done you should see that your list now contains a bunch of items, in fact, it should contain as many items as your **Class** has.
The **Repeater** node has created one instance of the list item per **Record**.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/quering-records-from-database/items-1.png)
</div>
The only problem is that the items does not show the content of the **Record**. To fix that we need to understand how the **Repeater** node deals with an **Array** of **Records**.
### Repeater and Records
To get the items to show the right data, we need to get hold of the **Record** holding the right data. We do this by using the [Record](/nodes/data/cloud-data/record) node. Add a **Record** node to the previously created list item. Click the node and make sure its **Class** is set to the class you want to display, in this guide we will point it to the `Task` Class.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/quering-records-from-database/record-1.png)
</div>
Now the **Id** of the **Record** comes into play - we need to set it to the **Id** corresponding to the **Record** that the list item represent. It will obviously be different for each list item. Luckily, the **Repeater** node solves this. It will assign the right **Id** to the right list item. See image below.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-data/quering-records-from-database/records-database-illustration.png)
</div>
The only thing we need to do is to tell the **Record** node to pick up its **Id** from the right place. Click the **Record** node again and change the **Id Source** to `From repeater`.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/quering-records-from-database/id-source.png)
</div>
Now if you hover on the **Repeater** you should see that its filled with data for one of the list items.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/quering-records-from-database/record-2.png)
</div>
### Connecting the Record properties
Finally, lets connect the data of the record - its properties - to our **Text** node and **Checkbox**.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/quering-records-from-database/record-3.png)
</div>
You should now be able to see your **Records** in your app.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/quering-records-from-database/items-2.png)
</div>

View File

@@ -0,0 +1,129 @@
---
title: Records Relationships
hide_title: true
---
# Record Relationships
## What you will learn in this guide
In this guide we will look into relationships between different **Records** in the database. This could for example be a _pointer_ stored in a **Record** that points to another **Record**. It could also be a "Many to Many" relationship where an **Record** can have a list of _pointers_ to other **Records**.
This is very useful when you deal with _relational data_, for example if you have a Post **Class** in your database, and you want to keep track of all the **Users** (another **Class**) that liked that post, a **Post** record can store relations to those Users in a property.
## Overview
We will first look at **Pointers**, i.e. a _One-to-many_ relationship. Then we will look at **Relations**, i.e. _Many-to-many relationshipts_.
It's recommended that you have some experience working with Backends, Databases and **Records** to get most out of this guide, so if you haven't already, going through the following guides before is recommended.
- [Creating a Backend](/docs/guides/cloud-data/creating-a-backend)
- [Creating a Class](/docs/guides/cloud-data/creating-a-class)
- [Creating Records](/docs/guides/cloud-data/creating-new-database-records)
## Relationships between Records
If we imagine an App where we have a couple of different **Classes**, say a _Post_ Class that contains blog posts and a _Comment_ Class with comments on these post. The **Records** in these classes need to be related and we need to be able to query these relations.
## Pointers - One-to-many relationships
The most simple relationship is a **Pointer**. This type of relationship is often referred to as a _One-to-many_ relationship. For example, a blog _Post_ will have many *Comment*s, but each _Comment_ only belong to one _Post_. To create this kind of relationship we use a property of a special kind called a **Pointer**.
A **Pointer** property can reference another **Record** of a certain Class via it's _Id_. In this case we want to reference the _Post_ that is the owner of a _Comment_. So in the _Comment_ class we create a property, let's call it `Owner`, and give it a _Pointer_ type and specify that it should point to _Post_ records.
?> This is called a _backward_ relationship, i.e. the _Comment_ points back to the _Post_. If you want to find all _Comments_ for _Post_ you will simply look for all _Comments_ pointing back to the _Post_ you are interested in.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/record-relations/create-pointer.png)
</div>
Now to set the pointer you can pass in the **id** of a **Post** Record. You could for example do this when you create a new _Comment_ **Record** . It's important the the **Record** you point to is of the right type. In this case a _Post_.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/record-relations/insert-pointer.png)
</div>
Later, if you want to retrieve all the _Comments_ for a _Post_ you simply use a **Query Records** node and ask for all **Comments** where the **Owner** property points to the specific comment.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/record-relations/query-pointer-1.png)
</div>
The specific **id** we are looking for, we provide through an input.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/record-relations/query-pointer-2.png)
</div>
You can inspect your relationships in the data browser, if you go ahead and open the _Dashboard_ for the cloud services and find the _Comment_ Class. There you can see the record _Id_ that the pointer currently points to, and you can click it to jump to that particular record.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/record-relations/comments-owner.png)
</div>
## Relation - Many-to-many relationships
Let's say that we introduce a new **Class** called _Group_, and a _Post_ can be part of many different *Group*s. In this case we cannot use the backwards pointing mechanism from the previous example. One single pointer from the _Post_ to the _Group_ would not work, since a _Post_ can be part of many _Groups_. Instead we need to use a concept called **Relation**. You need to start by creating a new property of the _Group_ **Record** that have the type _Relation_ and like pointers you need to specify the _Type_ and give it a name.
?> So in this case it becomes a _forward_ reference, the _Group_ has the relation property.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/record-relations/create-relation.png)
</div>
It can be a bit tricky in the beginning to get the hang of relations. One way to look at it is this: In this example, A _Group_ have a list of pointers to all its _Posts_ that belongs to it. So a specific _Group_ can easily find its _Posts_ by following these pointers. The inverse question, a _Post_ who wants to know which _Groups_ it belongs to (it can be more than one!) can query for all _Groups_ that have a pointer in its list that's pointing at them.
When you have a _Relation_ property on a **Record** you need to use the nodes [Add Record Relation](/nodes/data/cloud-data/add-record-relation) and [Remove Record Relation](/nodes/data/cloud-data/remove-record-relation) to manage them. In these nodes you need to provide the **id** of the **Record** that has the relation property (Group in our case) and the **id** of the **Record** you want to add or remove to the relationship.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/record-relations/record-relation-1.png)
</div>
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/record-relations/record-relation-2.png)
</div>
In the same manner as with _Pointers_ you can go to the _Dashboard_ of the cloud services and find the relations of your models in the table. You can click _View relation_ to get a table of the relations for this particular model.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/record-relations/view-relation.png)
</div>
Finally you need to be able to query the relations. You have two cases:
1. Either you want to find all _Post_ that are related to a _Group_. The you should create a **Query Records** of the class _Post_ (you want _Posts_ back from the query). You can then set up the filter according to the image below
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/record-relations/query-relation-1.png)
</div>
Note that you have to specify the name of the relation property in the class having the property (_Group_ in this case) as well.
2. If you want to make the inverse query, that is you want to ask for all _Groups_ that relates to a post, you create a **Query Records** node with the class _Group_ (you want _Groups_ back from the query). have a _Post_ **Record** (you have the _Id_ of a _Post_ **Record**) and you want to find all *Group*s that it belongs to you would use this javascript query in a **Query Records** that is set to the **Group** class.
```javascript
where({
posts: { pointsTo: Inputs.PostId },
})
```
As you can see above you must also make sure that you use the correct relation field, in this case _posts_ on the _Group_ collection.

View File

@@ -0,0 +1,102 @@
---
title: Updating Records
hide_title: true
---
# Updating Records in the Database
## What you will learn in this guide
In this guide you will learn how to use the [Set Record Properties](/nodes/data/cloud-data/set-record-properties) node to update data in the Database.
## Overview
We will go through the following steps in this guide
- Update previously queried **Records**
To get most out of this guide, it's best that you are already familiar with how to set up a cloud backend, create **Classes** and **Records** and query them. You can quickly learn that by going through the following guides:
- [Creating a Backend](/docs/guides/cloud-data/creating-a-backend)
- [Creating a Class](/docs/guides/cloud-data/creating-a-class)
- [Creating Records](/docs/guides/cloud-data/creating-new-database-records)
- [Query Records](/docs/guides/cloud-data/quering-records-from-database)
## Updating Records in the database
It's simple to update **Records** in the database using the [Set Record Properties](/nodes/data/cloud-data/set-record-properties) node. You basically make sure the **Id** is set to the right **Record**, set the properties to what you want them to be and signal the **Do** input.
To try it out, make sure you have a cloud service active for your project. Set up a **Class** in the Database with some **Records** in. Finally Query some items from the **Database**. If you follow the previous "Working With Cloud Data" guides you can use the simple Task list created there. It looks something like this:
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/updating-records/items-2.png)
</div>
The app has two components, the main component called "App" and a List Item called "Task Item".
The main component that looks like this:
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/updating-records/orig-app.png)
</div>
And the list item like this
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/updating-records/record-3.png)
</div>
A clear shortcoming of this small app is that when you complete a task and check its checkbox the database wont actually update. The `isDone` property will not change.
You can double check that by clicking a few items and reload the app. The changes are only visual, the underlying data is not changed.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/updating-records/checkbox-anim.gif)
</div>
### The Set Record Properties node
To update a **Record** you use the [Set Record Properties](/nodes/data/cloud-data/set-record-properties) node.
Add the node to your List Item. Click it, to edit its properties, and make sure the correct **Class** is selected.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/updating-records/list-item-1.png)
</div>
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/updating-records/set-record-prop-1.png)
</div>
Also note the option `Store to`. You can chose to store changes either locally and in the cloud, or only locally. Let it be set to `Cloud and locally` as we our changes to be stored directly in the database.
Sometimes it makes sense to only store the changes locally at first. For example, if you have a form with multiple fields and a "Save" button, it might make sense to first store only locally and when the user presses "Save" to store all changes at once.
Now we need to make sure the **Id** of the **Set Record Properties** are set correctly to the **Id** that the **Repeater** provides.
<div className="ndl-image-with-background">
![](/docs/guides/cloud-data/updating-records/set-record-prop-2.png)
</div>
Now the only two things left to do is to connect the **Checkbox** to the `isDone` property and trigger the **Do** signal of the **Set Record Properties**. We want to save whenever the state of the **Checkbox** is updated, so lets connect the **Changed** signal coming out of the **Checkbox** to **Do**.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-data/updating-records/list-item-2.png)
</div>
We're done! Click on your different tasks and double check that they are correctly saved by bringing up your **Dashboard** inspect the `isDone` properties. Don't forget to press `Refresh` in the **Dashboard** to update the view.

View File

@@ -0,0 +1,199 @@
---
title: Emal Verification
hide_title: true
---
# Email Verification
Cloud functions play an important role when creating different log in and sign up flows. Using the nodes [Sign Up](/nodes/data/user/sign-up), [Log In](/nodes/data/user/log-in) and [Log Out](/nodes/data/user/log-out) you can create the most basic flow that will have the user sign up with a username, optionally email, and password and log in with username and password.
:::note
It's common to use email for both **username** and **email** when signing up, so you only ask the user for email and password, one less thing to remeber right.
:::
Once logged in you can use the [Access Control](/docs/guides/cloud-data/access-control) functions of the cloud database to control what a user has access to and not. The built in role system will allow you to create features like teams/groups of users.
This is a great way to get started and focus on building your application. But once you start getting to the point where you want to expose it to more users often you need a more solid sign up and log in flow. The first addition is likely the need to verify the email of users and allow them to reset the password and this is what we will cover in this guide.
There is a project template that contains a more complete sign up and log in flow that also covers sending emails to users on sign up, editing the profile etc, and it uses cloud functions to do some of these things that cannot be performed from the frontend for security reasons (cloud functions always have full access to the database).
You can either start a new project from the template, or you can import the cloud functions into your existing project. We will review them here in this guide, case by case.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/email-verification/signup-template.png)
</div>
## Settings
In order to make the email verification process work there are a few configuration parameters that need to be provided. This is done by opening up the dashboard for your **Cloud Serivce** and navigating to the **Config** tab using the sidebar. You can learn more about config parameters by looking at the [Config](/nodes/data/cloud-data/config) node. The configuration parameters you need a are shown below:
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/email-verification/settings.png)
</div>
- `EmailVerificationDomain` Here you need to enter the domain where your application is deployed starting with `https://`. This is used for the links in the verification emails. When running locally this will automatically be `http://localhost:8574`, this is where the local Noodl web server is running.
- `EmailVerificationFrom` This is where you put the email address that should be used as the "from" email when sending verification emails to users. It's important that this is a valid email with **Send Grid** (the email sending service we use for the application).
- `SendGridAPIKey` The project template and email verification prefabs use [SendGrid](https://sendgrid.com/) as an email service, to use it you need to sign up and get an account (it's free to test). Then create an API Key and put it in the config. More info on how to use Send Grid with Noodl can be found [here](/library/prefabs/sendgrid).
## Signing up
Signing up is done with the [Sign Up](/nodes/data/user/sign-up) action node. After the user has successfully signed up the cloud function **Send Verification Email** is called. This function will send an email to the address provided by the user.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/email-verification/sign-up-1.png)
</div>
Let's take a closer look at the cloud function. There is no need to dive into the details but it's good to review the main flow and blocks. The cloud function is found in the cloud function tab, in the **Sign Up** folder, it's called **Send Verification Email**. The first this when the function is started is that a **Request Email Verification** action component is used.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/email-verification/sign-up-2.png)
</div>
This action create a secret token (a random string of characters, sort of a temporary password) that it stores with the current user. This token will later be sent to the user as part of an email. If the email is already verified a signal will be emitted on **Email Is Verified** which we will return as an error result for the cloud function.
The next part is actually sending the email to the user. This is done in the function with the **Format Email** and **Send Email** action components.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/email-verification/sign-up-3.png)
</div>
The **Format Email** action takes as input the verification token and the email of the user and creates an email with a link. You can look at the properties of the **Format Email** node to see the content of the email.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/email-verification/sign-up-4.png)
</div>
As you can see it creates an email containing an HTML link, this link uses some fancy template syntax.
- **$Domain** This will be replaced by the format email node to the domain where your application is deployed, so that the link will take you back to the app. More on this later.
- **{Token}** This is the generated token from before.
- **{Email}** This is the email for the user, it will be used to fetch the user and marked the email as verified in the next step.
The **Format Email** node outputs the final email with the correct values for the above placeholders insterted. This email content is then sent to the **Send Email** node that actually sends the email to the user.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/email-verification/sign-up-5.png)
</div>
That's it. Now the user should have a fresh email with the subject **Email Verification!** in it's inbox. You can edit the **Subject** property of the **Send Email** action.
## Verifyng the email
We need one more thing in place for the email verification flow to work. We need the page that the link in the verification email points to. After the **Format Email** action have formatted the email template and inserted the **Token**, **Email** and **Domain** the resulting link will look something like this.
`<a href="https://your-app.sandbox.noodl.app/verify-email?token=abc&email=user@email.com">verify</a>`
This little thing will send the user back to your app (remember you can use `http://localhost:8574` as domain for testing, before your app is deployed) and specifically to the page `/verify-email`. So, let's take a look at the page in the project template.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/email-verification/verify-email-1.png)
</div>
There is a lot of stuff here but the important things is the **Page Inputs** where we get the **Token** and **Email** as part of the query parameters in the link (the stuff after the `?` in the link), these are passed to the **Sign Up / Verify Email** cloud function that is called as soon as the page is loaded with the **Did Mount** signal.
If it succeeds, the email was verified and the user is sent to the log in page of the app with the **Navigate** node and a toast message is shown. If it fails, a message is displayed on the screen and a toast is shown using the **Show Toast** component (you can find this among the prefabs and install it into your project, same for the **Loading Spinner**).
Once logged in to your app you can inspect the user object by hovering over any **User** node. Here you can see some properties that have been set by the email verification cloud functions.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/email-verification/verify-email-2.png)
</div>
The most important is the **emailVerified** property of the user, this indicates if the user have verified their email of not. In the sign up project template the user is actually send to the home screen of the app even if the email address is not verfied and a banner is shown. You could for instance only enable certain parts of the application if the user have verified their email.
If the email was not received properly of if the user would like to have another verification email sent you can simply call the **Sign Up / Send Verification Email** cloud function again.
:::note
If you update the email with the **Set User Properties** action node, it will automatically switch the **emailVerified** property of the user to false.
:::
## Reset Password
Resetting a user password when it's been lost follows the same pattern as sending an email for verification. First you need to present some sort of UI where the user can enter their email address to recover their password.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/email-verification/reset-password-1.png)
</div>
There is a function called **Sign Up / Request Password Reset** that simply accepts an **Email** and it can be called without the user being logged in.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/email-verification/reset-password-2.png)
</div>
The cloud function follow pretty much the same pattern as when sending email verifications. It will send an email to the user with a link containing a secret token just like when veryfing the email address.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/email-verification/reset-password-3.png)
</div>
The **Request Password Reset** action will generate the secret token which is passed to the **Format Email** along with the users email. This time it will generate a link to a page called `/reset-password`. You can edit the content of the email in the **Format Email** node.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/email-verification/reset-password-4.png)
</div>
The resulting link will look something like this:
`<a href="https://your-app.sandbox.noodl.app/reset-password?token=abc&email=user@email.com">link</a>`
The link will take the user to the `/reset-password` page which can contain a **Text Input** where the user can provide the new password.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/email-verification/reset-password-6.png)
</div>
When the user hits the reset button we will call the **Sign Up / Reset Password** cloud function by supplying the secret token and user email that is received via the query parameters of the link and the **Page Inputs** node.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/email-verification/reset-password-5.png)
</div>
Provided that the secret token is correct and have not expired (tokens are valid for 24 hours) the password will be updated. You can then send the user back to the **Log In** page.
That's it, this is how you use cloud functions to create an email verification and password reset flow. You will use cloud functions for a lot of user management tasks that need to be performed on the backend with full database access.

View File

@@ -0,0 +1,120 @@
---
title: Introduction to Cloud Functions
hide_title: true
---
# Introduction to Cloud Functions
**Cloud Functions** in Noodl is a way to create logic that run in the cloud using the same techniques that you use when building business logic in your frontend, namely connecting logic and action nodes into flows, injecting Javascript where needed.
A **Cloud Function** is just another component in your project, but they don't live among the frontend components. Instead you find them in the **Cloud Functions** tab.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/introduction/cloud-functions-tab.png)
</div>
You create a new **Cloud Function** component by clicking the **+** icon in the sidebar and selection **Cloud Component Function**.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/introduction/new-cloud-function.png)
</div>
You give the cloud function a name and click **Create**.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/introduction/cloud-function-name.png)
</div>
You can also create **Folders** and **Logic Components** just like you do on the frontend to keep things organised and to seperate functionality into reusable logic components.
## Anatomy of a cloud function
When you create a new **Cloud Function** you will end up with two nodes that are the backbone, the [Request](/nodes/cloud-functions/request) and the [Response](/nodes/cloud-functions/response) nodes.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/introduction/cloud-function-empty.png)
</div>
First let's look closer at the **Request** node. When a cloud function is called from the client this is where the logic flow starts.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/introduction/cloud-function-request.png)
</div>
The logic flow is initiated by the <span class="ndl-signal">Received</span> signal. So the first actions that you want the cloud function to perform should be connected to this signal. There are also a couple of important properties on the **Request** node that we should look at:
**Allow unauthenticated**. This is an important property, by default all cloud functions need the user to be logged in to be accessible. But if you for some reason want the function to be callable without a user you can check this property. You must be careful since all cloud functions have full access to your database and can do things that might be limited in the client for security reasons.
**Parameters**. Here you add the parameters for your cloud function, these will become outputs on the **Request** node and inputs on the **Cloud Function** node in the frontend that you use to call your functions.
In the very simple example above we use the **Set User Properties** node to set the password of a user, this obviously needs a logged in user (otherwise the **Set User Properties** node wont work), we accept the new password as a parameter and trigger the action node on the **Received** signal.
When a **Cloud Function** completes it needs to either succeed or fail, this is done using the second important cloud function node, namely the **Response** node.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/introduction/cloud-function-response.png)
</div>
When the function completes you should send a **Send** signal on the **Response** node. By default this will result in a successful completion of the function with no result parameters. If you need to send a failure you can change the **Status** property to **Failure** and provide an error message.
If you need to return a result to the client you specify result **Parameters** in the properties of the **Response** node and make appropriate connections.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/introduction/cloud-function-results.png)
</div>
## Calling cloud functions
When your **Cloud Function** is in place you will need to call it from your frontend, this is done using the [Cloud Function](/nodes/data/cloud-data/cloud-function) node. First you need to pick the cloud function that you want to call using the dropdown in the node properties.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/introduction/cloud-function-call-props.png)
</div>
Then you can hook up the node. You call the function by sending a signal to the <span class="ndl-signal">Call</span> signal input.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/introduction/cloud-function-call.png)
</div>
If you have any parameters in your **Request** node of the selected cloud function then they will show up on the **Cloud Function** node as inputs, same for result parameters in the **Repsonse** nodes.
When your cloud function has completed running it will result in either a <span class="ndl-signal">Success</span> or <span class="ndl-signal">Failure</span> signal.
## Deploying
This is the basics of cloud function, keep reading the guides in this section to learn more about the details and different use cases. Finally you will want to deploy your application, and for the cloud functions to work properly you need to make sure you have a cloud services selected when deploying.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/introduction/cloud-functions-deploy.png)
</div>

View File

@@ -0,0 +1,54 @@
---
title: Javascript in Cloud Functions
hide_title: true
---
# Javascript in Cloud Functions
Not all nodes are available to cloud functions but in terms of writing Javascript the **Function** and **Expression** nodes are available. However there are some caveats, most notably when deployed cloud functions are not running in a browser they are running in the cloud and they have a much more limited runtime environment. This means that you cannot access most of the browser APIs, the cloud runtime is limited to:
- **Core Javascript Objects and Functions** The Javascript language contains a set of core objects and functions that are all available to the cloud runtime. More info [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects).
- **The Noodl API** A subset of the Noodl API is available to cloud functions. Read more about the Noodl APIs [here](/javascript/overview). Some of the functions and objects are labeled **Only available on the frontend** and some are labeled as **Only available in cloud functions**, the rest are shared between frontend and cloud functions.
## Fetch
A subset of the **Fetch API** available on the browser also works in cloud functions, which is very useful for making HTTP requests to external services.
**`fetch(url,options)`**
The **Fetch API** first takes the endpoint of the HTTP request and then an object with options. Here are a few examples, first making a simple POST request to an endpoint:
```javascript
const res = await fetch("https://some-endpoint",{
method:'POST',
headers:{
'content-type':'application/json'
},
body:JSON.stringify({
someParameters:"hello"
})
})
if(res.ok) {
Outputs.Success()
}
else Outputs.Failure()
```
## The Request Object
In Javascript in your cloud functions you can access the request object containing data on the current request being handled.
```javascript
const request = Noodl.Objects.Request;
request.UserId // Contains the user if of the user that called the cloud function, if authenticated
request.Authenticated // Will be true if this call is authenticated
request.Parameters // An object with the parameters of the current cloud function request
request.Headers // An object with the HTTP headers of the current cloud function request
```

View File

@@ -0,0 +1,80 @@
---
title: Logging and Debugging
hide_title: true
---
import useBaseUrl from '@docusaurus/useBaseUrl'
# Logging and Debugging
When building cloud functions it's important to know how to spot errors and problems quickly. This is where logging and debugging comes in.
## Running locally
When you are running your application in the Noodl editor all cloud functions are run on your local computer. The are accessing the active cloud service of the project, just as they will when deployed. The easiest way to find errors in your cloud functions is to inspect data and signal connections in your node graph as your functions are running locally.
<div className="ndl-video">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/cloud-logic/logging/cf-inspect.mp4")}/>
</div>
Simply bring up the cloud functions tab, choose the function you want to inspect and then trigger it from the app preview. You will see the data and signals flowing through your graph and you can inspect the values afterwards just like in your frontend components.
Sometimes this is not enough and we need to open the **Cloud Runtime Debugger**, you can launch it from the top of the sidebar in the cloud functions tab.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/logging/open-debug.png)
</div>
This will open a web debugger, just like when debugging your front end. When your cloud functions run locally they run in a web runtime which means that you have access to all the same debug tools. Most notably you can debug network calls from your function, as well as the logging to the console, more on that in a bit.
The **Refresh cloud functions** button above is also very useful if you want to clear error messages and inspect data and run your function clean.
If you want to call your cloud functions from external tools while running locally you can do this by accessing the local Noodl clound functions server directly.
```bash
curl -X POST http://localhost:8577/functions/my-func -H 'Content-Type: application/json' -d '{"someParameter":"value"}'
```
This can be very useful when working with e.g. webhooks as they can be tested locally without having to deploy.
## Logging
Another important tool to make sure your functions are running as expected is logging, especially when finding problems in deployed applications. When you are running locally you will find the logs in the **Cloud Runtime Debugger** under the **Console** tab.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/logging/console-log.png)
</div>
There are three functions you can use to log from your cloud functions:
```javascript
// These two both log messages as "info" severity
console.log('Some message')
console.info('Another message')
// This is logged with "error" severity and should be reserved for
// critical issues. So they will be easier to find.
console.error("An error occured")
```
When you have deployed your application you can find the logs in the dashboard for the cloud service that you have deployed to.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/logging/log-dashboard-info.png)
</div>
The logs are divided into **info** and **error** depending on which log function you used above. If you make sure to reserv error for more critical issues they will be easier to find.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/logging/log-dashboard-error.png)
</div>

View File

@@ -0,0 +1,212 @@
---
title: Scheduled Jobs
hide_title: true
---
# Scheduled Jobs
One very useful task for cloud functions are scheduled jobs. This is logic that you want to run in the cloud at specific intervals, such as every 15 minutes, hourly or daily. These jobs typically take care of housekeeping tasks that run in the "background" of your application.
Let's say for this example that we have a class in the database where a lot of records gets created as the application is running and to not use unnecessary data storage we want to prune the records that are older than 48 hours.
## The job cloud function
We start by creating a simple cloud function that will do the cloud job. Let's call it **Cleanup**.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/scheduled-jobs/cleanup-1.png)
</div>
In this function we add a small logic flow that first find all **Photos** that are older than 48 hours. This is done with a [Query Records](/nodes/data/cloud-data/query-records) node and a date filter, passing in the date we want to compare **createdAt** too via a small **Function** snippet.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/scheduled-jobs/cleanup-nodes-1.png)
</div>
The **Date** object to compare to is calcuated as such (date right now, and back up 48 hours). We want all **Photo** records that have a date that is less (earlier) than this.
```javascript
Outputs.FilterDate = new Date(Date.now() - 48*60*60*1000)
```
If successful the photos are passed into a handy node called [Run Tasks](/nodes/data/run-tasks) which will perform a logic component task for each item in the array that is fed to it, reporting **Done** when the are all processed. We need to create the logic component, I called it **Delete Task** and added it as a child component to the cloud function.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/scheduled-jobs/cleanup-delete-0.png)
</div>
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/scheduled-jobs/cleanup-delete-1.png)
</div>
The **Delete Task** is very simple, it uses the [Delete Record](/nodes/data/cloud-data/delete-record) node triggered when the **Do** is sent. This signal is sent by the **Run Tasks** node.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/scheduled-jobs/cleanup-delete-2.png)
</div>
The **Run Tasks** node will create an instance of the task component (**Delete Task**) for each item in the array (that is for each **Photo** we want to delete) and just like the [Repeater](/nodes/ui-controls/repeater) node you can specify that the **Delete Record** node should operate on the current record.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/scheduled-jobs/cleanup-delete-3.png)
</div>
Then we just need one more thing for out **Cleanup** cloud function and that is to make sure it can be called without **Authentication**, we will get back to this a bit later. Make sure the **Request** node in the **Cleanup** cloud function has this property checked.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/scheduled-jobs/cleanup-noauth.png)
</div>
## Testing and deploying
The easiest way to test your background job is to trigger it manually from the UI of your application. Add a button somewhere (maybe in an admin panel of your app) and simply run the function. This will allow you to test if properly and debug it in the Noodl editor before deploying.
<div className="ndl-image-with-background m">
![](/docs/guides/cloud-logic/scheduled-jobs/cleanup-test-1.png)
</div>
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/scheduled-jobs/cleanup-test-2.png)
</div>
When it's working to your liking, deploy it to your backend. Take a look in this [guide](/docs/guides/cloud-logic/introduction#deploying) to learn more about deploying.
## Scheduling the job
When the cloud function is deploy we need to schedule it to be run at the interval we want. You can run a deployed cloud function from outside of Noodl, this is very handy for tasks like handling paymnents with external providers etc, and it's super useful for scheduling cloud jobs too. First, find your cloud service in the cloud services tab, and look for **Manage Cloud Service**.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/scheduled-jobs/bg-schedule-1.png)
</div>
<div className="ndl-image-with-background m">
![](/docs/guides/cloud-logic/scheduled-jobs/bg-schedule-2.png)
</div>
This will open a popup showing you information about the selected cloud services, we are looking for the **Endpoint**, this is the HTTP address you use to access your cloud service.
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/scheduled-jobs/bg-schedule-3.png)
</div>
The endpoint will have the following format:
```
https://backend.noodl.cloud/xyz/123
```
With the endpoint in hand you can go ahead and set up the scheduling for your cloud job. There are a wide range of tools for scheduling HTTP calls and my absolute favorite is [cron-job.org](https://cron-job.org/), it's clean, simple and free.
Once you have created an account and sign in, look for the **Create Cronjob** button.
<div className="ndl-image-with-background m">
![](/docs/guides/cloud-logic/scheduled-jobs/cron-1.png)
</div>
First you provide the endpoint of the cloud function that is your backgroud job that you want to schedule, you use the endpoint for above and add ```/functions/{function-name}```
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/scheduled-jobs/cron-2.png)
</div>
:::note
Some tools don't handle urls with big and small cases to it might be a good idea to just use small cases in the names of your cloud functions and no spaces or other special characters.
:::
Move on to choosing the schedule interval for your background job:
<div className="ndl-image-with-background m">
![](/docs/guides/cloud-logic/scheduled-jobs/cron-3.png)
</div>
:::note
Don't schedule your tasks too often. In Noodl hosted cloud services (and generally self hosted too) you are billed for the amount of time your cloud function runs, and if you are using a free plan and run too many cloud functions they will be throttled after a while and your application performance will suffer.
:::
Now move over to the **Advanced** tab for some additional settings. Mainly that you need to change the **Request Method** to **POST** as that is how you invoke cloud functions in Noodl.
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/scheduled-jobs/cron-4.png)
</div>
That's it, you can now test your function using the **Test Run** button and it should run successfully. Then go ahead and create it.
<div className="ndl-image-with-background m">
![](/docs/guides/cloud-logic/scheduled-jobs/cron-5.png)
</div>
Now your cloud function will be scheduled and you can just sit back and watch as your photos are clean up (or whatever you choose to do in your background job) nicely.
## Security
One final note on security. Above we set the cloud function to **allow unauthenticated requests**, this means that anyone can call this function whenever. It's not a huge problem since it doesn't do anything sensitive, it will simply prune our photos a bit more often. But it might run up our bill. So let's add a secret key that is needed to make the call.
Simply add parameter to your cloud function called **Secret**, and add a small logic in the beginning of your function to verify it (you can pick any secret you like).
<div className="ndl-image-with-background xl">
![](/docs/guides/cloud-logic/scheduled-jobs/security-1.png)
</div>
If the secret provided when calling this function does not match, then send an error response back.
<div className="ndl-image-with-background m">
![](/docs/guides/cloud-logic/scheduled-jobs/security-2.png)
</div>
This will make sure that no one can call your function (or at least, it won't do any possibly expensive work) without knowing your secret. Finally, provide the same secret when scheduling your background job, this can be done in the **Advanced** tab in [cron-job.org](https://cron-job.org/).
<div className="ndl-image-with-background l">
![](/docs/guides/cloud-logic/scheduled-jobs/security-3.png)
</div>
There you go, now you have a scheduled cloud job up and running. Calling cloud functions from external services like this is a very useful pattern to integrate Noodl with other services, it will be a recurring pattern in other guides so it's good to know.

View File

@@ -0,0 +1,9 @@
---
title: Collaboration
hide_title: true
---
# Collaboration
Noodl has a built-in git client to handle version control and make it easy to follow.
### [Start learning about version control of Noodl apps](/docs/guides/collaboration/version-control)

View File

@@ -0,0 +1,182 @@
# Version control
## Introduction
Version control has three main use cases:
- Collaborating with team members
- Working with different versions of your project in parallel (what's known as "branches")
- Backup. All versions are backed up in the cloud
<div className="ndl-image-with-background s">
![](/docs/guides/collaboration/version-control/intro.png)
</div>
Noodl uses [Git](https://git-scm.com) as the underlying version control system. The project folder is a git repository.
Users comfortable with git can use external git clients to view their Noodl projects.
## Enabling version control
Projects will automatically get a version control repository in the cloud. You can use it locally without sending data to the cloud, but to collaborate you have to push your changes, which will upload the project to Noodl's cloud.
:::danger
A project that isn't pushed isn't available to collaborators or if you log into another computer.
Same goes for the changes you make, you have to push them to upload to the cloud.
:::
Read more about how to push your project below.
## The version control panel
The version control panel is where you perform all actions related to version control.
This includes actions such as pushing new changes to your collaborators,
pulling down the latest version, seeing differences between versions, merging branches, and more.
<div className="ndl-image-with-background">
![](/docs/guides/collaboration/version-control/initial-state.png)
</div>
## Commiting your changes
When there are changes in your project and you can commit your changes to the version control system. This creates a commit, which is a snapshot of the state of your entire project. This can then be pushed to share your changes with collaborators (or yourself if you work on multiple computers).
<div className="ndl-image-with-background">
![](/docs/guides/collaboration/version-control/status-commit.png)
</div>
Noodl will now show you all the components and files that have changed, and by clicking them you can see the difference compared to the previous version.
<div className="ndl-image-with-background l">
![](/docs/guides/collaboration/version-control/first-commit.png)
</div>
After the changes have been commited the commit is now visible in the history tab.
<div className="ndl-image-with-background">
![](/docs/guides/collaboration/version-control/first-push.png)
</div>
Push your changes by clicking the button at the top that says "Push 1 local commit".
## Pulling down changes
Noodl checks if there are any changes to pull down when the version control panel is opened.
You can also manually check for updates by clicking the update icon
<div className="ndl-image-with-background">
![](/docs/guides/collaboration/version-control/update.png)
</div>
A new version will show be displayed as "Remote commits" in the history tab. These are versions that you haven't pulled down yet.
<div className="ndl-image-with-background">
![](/docs/guides/collaboration/version-control/first-pull.png)
</div>
The button at the top lights up and shows how many remote commits are available. Click it to pull down the changes. You can click on the commits in the history tab to see the changes you're about to pull.
Any local changes you have, that aren't pushed, will still be there after pulling down a new version. They will be applied to the new versions, which leads to the next subject, conflicts.
## Conflicts
Sometimes you and your collaborators might change the same parameter on the same node. In those cases, Noodl doesn't know which change to apply and will ask you to resolve the conflicts.
<div className="ndl-image-with-background l">
![](/docs/guides/collaboration/version-control/conflicts.png)
</div>
You can resolve conflicts by choosing to keep your change or overwrite your change with the one from your collaborator.
## Branches
Branches can be used to develop different parts of your application in parallel, that can later be merged.
Here are a few common use cases
- One branch for the stable version of your app, and another one for the development version
- A new feature that's being worked on can be developed in a separate branch, and later merged into the main branch when it's ready
- Explore different designs variations of your app, deploying them independently, and later merge in the variation that was the most successful.
- ...and there are many more
### Creating a branch
Create a branch by clicking the '+' icon in the version control panel's branch dropdown.
<div className="ndl-image-with-background">
![](/docs/guides/collaboration/version-control/branch-create.png)
</div>
After a branch has been created Noodl will switch to the new branch. Any changes in your project that weren't pushed will be moved to this new branch.
Changes you push will now belong to the new branch, and won't affect the other branches.
### Switching branches
Switch between branches by clicking on a branch. Switching requires you to have no local changes. There are three ways to solve this:
- Commit your changes to the current branch
- Delete your changes
- (Advanced) Stash your changes
<div className="ndl-image-with-background l">
![](/docs/guides/collaboration/version-control/switch-branch.png)
</div>
### Merging branches
Changes from one branch can be merged into another branch. In this example, the "my-new-feature" branch is being merged into the "main" branch.
<div className="ndl-image-with-background">
![](/docs/guides/collaboration/version-control/merge-1.png)
</div>
Clicking "Merge into main" will show a preview of the changes that are being merged in. Noodl is visualizing the changes as they will look after the merge has been done.
<div className="ndl-image-with-background l">
![](/docs/guides/collaboration/version-control/merge-2.png)
</div>
Click "Merge branches" to perform the merge.
When the merge is done, it is done locally, and won't yet be shared with collaborators. You can now choose to push the changes, or continue working locally.
## Deleting a branch
After a branch has been merged into another branch it can be a good idea to delete it if it won't be used anymore.
Note that deleting branches will delete branches both locally and remotely. If other collaborators have done work on the branch you just deleted, their local copy of the branch will still be intact and they can choose to push their changes to restore the branch.
The main branch can't be deleted.
<div className="ndl-image-with-background">
![](/docs/guides/collaboration/version-control/branch-delete.png)
</div>

691
docs/guides/data/arrays.mdx Normal file
View File

@@ -0,0 +1,691 @@
---
title: Arrays
hide_title: true
---
import ReactPlayer from 'react-player'
import ImportButton from '/src/components/importbutton'
# Arrays
## What you will learn in this guide
In this guide we will look more closely on how to use **Arrays** in Noodl. **Arrays** are fundamental for lists in Noodl and useful for many other things as well.
Noodl have a number of nodes related to Arrays. For accessing and modifying there are:
- [Array](/nodes/data/array/array-node)
- [Create New Array](/nodes/data/array/create-new-array)
- [Insert Object Into Array](/nodes/data/array/insert-into-array)
- [Remove Object From Array](/nodes/data/array/remove-from-array)
- [Clear Array](/nodes/data/array/clear-array)
For mapping and filtering an **Array** the [Array Filter](/nodes/data/array/array-filter) and [Array Map](/nodes/data/array/array-map).
You will also learn to use the [Static Array](/nodes/data/array/static-array) node which is very useful for handling static list data in your App.
## Overview
This guide will walk through the following topics
- What's a Noodl **Array**
- Creating **Arrays**, statically and dynamically
- Adding and Removing objects from an **Array**
- Using **Static Arrays**
- Using Arrays in Lists and Dropdowns
- Filtering Arrays
Arrays are used all the time in Noodl. Hence they occur in many guides. It's suggested to check out the [List Basics](/docs/guides/data/list-basics) guide to learn more about how to display **Arrays** as lists. Also when querying data from a **Backend Database** the data is returned in an **Array** hence it could be interesting to check out some fo the [Cloud Data](/docs/guides/cloud-data/overview) guides in conjunction with this guide.
Before reading this guide, it's a good idea to have a basic understanding of what an [Object](/nodes/data/object/object-node) is, which is described in [this](/docs/guides/data/objects) guide.
## What's a Noodl Array
A Noodl **Array** is an ordered list of items, **Objects** or (if the array is the result of a database query) [Records](/nodes/data/cloud-data/record). An **Array** i Noodl is represented with an **Array** node.
:::note
Note that a Noodl **Array** is not the same as a JavaScript array. They are easy to convert between but you cannot access a Noodl **Array** using JavaScript Array functions. Instead there is a Noodl Array API that will be covered later in the guide.
:::
### The Id of an **Array**
As with the [Variable](/nodes/data/variable/variable-node) and **Object**, an **Array** is uniquly identified by its **Id**. Any node referencing an **Array**, through an **Array** node or **Insert Object Into Array** etc, do it through an **Id**.
Therefore it's quite common to connect the **Id** from one node, e.g. an **Array** node, to the **Id** of another node, for example an **Insert Object Into Array** to make sure they are operating on the same **Array**. You can also just write the **Id** into the nodes if the **Id** is static.
Below are three typical cases of how to use the **Id** of an **Array** and make sure a **Clear Array** node is acting on the same **Array**.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/arrays/id-1.png)
</div>
In the first case the **Id** of an **Array** is stored in a **String** node (or for example a **Variable**) which is then used to refer to the **Array** in different node.
In the second case the **Id** is set directly in the **Array** node, and then that **Id** is used to point the **Clear Array** to the correct **Array**. The **Id** is set in the property panel.
<div className="ndl-image-with-background">
![](/docs/guides/data/arrays/id-2.png)
</div>
In the last case the **Id** is generated from a **Create New Array** below (more on that later) and then used to make sure the **Clear Array** node operates on the correct node.
### The Items of an **Array**
The **Items** of an **Array** are either **Objects** or **Records**. An item can be a member of multiple **Arrays**. This means that two different **Arrays** (i.e. with different **Ids**) can have exactly the same items, but they are still different **Arrays**. An item can only occur once in an **Array**.
When you hover over an **Array** node in the node canvas you will see how many items the **Array** currently has.
<div className="ndl-image-with-background s">
![](/docs/guides/data/arrays/items-1.png)
</div>
You can easily assign items to an **Array** by setting the **Items** input of an **Array** node. This is often done used for example when you do a query to the database through a [Query Records](/nodes/data/cloud-data/query-records), or handling the result of a [Array Filter](/nodes/data/array/array-filter) or [Static Array](/nodes/data/array/static-array). See three examples below.
<div className="ndl-image-with-background l">
![](/docs/guides/data/arrays/items-2.png)
</div>
Most often you assign items to an **Array** because you want to process them (perhaps add or remove some more items) or keep track of them to be used later.
## Creating Arrays
As you saw in the example above there are two main ways of creating an **Array**.
1. **Static creation** - You simply add an **Array** node in you node graph. You can give the **Array** an **Id** (as discussed above) or simply leave it blank, which means an **Id** will be assigned automaticallt to the Array.
2. **Dynamic creation** - You create a new **Array** on a certain event, for example using a **Did Mount** signal or a **Click** signal.
:::note
If you give your **Array** a static **Id** and if you have multiple instances of the component with the **Array** node (for example a couple of list items), the same **Array** will be referenced in all those components. This may or may not be what you want, so keep that in mind.
:::
## Creating a Recipe App
Now let's start playing around with **Arrays**. Let's create a small recipe app. The overall idea would be to have a list of Recipes (stored in an **Array**) and each recipe have a bunch of ingredients, also stored in an **Array**. So in the end we'll have an **Array** with **Arrays**.
Let's start with some sample data! Create a new project, using the "Hello World"-template. Then create a **Static Array** node. Make sure it's set to `JSON` format.
<div className="ndl-image-with-background s">
![](/docs/guides/data/arrays/static-array-1.png)
</div>
Then copy and paste the following into the JSON data.
```json
[
{
"name": "Swedish Pancakes",
"description": "A simple tasty recipe of classic Swedish pancakes (8-10 pcs)",
"containsMeat": false,
"ingredients": [
{ "amount": "150 g", "name": "Wheat flower" },
{ "amount": "1/2 tsp", "name": "Salt" },
{ "amount": "3", "name": "Eggs" },
{ "amount": "600 g", "name": "Milk" },
{ "amount": "1 tbsp", "name": "Butter" }
],
"instructions": "Mix flower, salt, milk and eggs well. Fry thin pancakes on medium heat in butter, 3-4 minutes on each side"
},
{
"name": "Swedish Meatballs",
"description": "A swedish classic. Eat with potatoes, gravy and lingonberries.",
"containsMeat": true,
"ingredients": [
{ "amount": "500 g", "name": "Minced meat" },
{ "amount": "1", "name": "Egg" },
{ "amount": "1 clove", "name": "Garlic" },
{ "amount": "3 tbsp", "name": "Bread crumbs" },
{ "amount": "1/2", "name": "Yellow Onion" },
{ "amount": "2 tsp", "name": "Concentrated beef stock" },
{ "amount": "2 tbsp", "name": "Full cream" },
{ "amount": "1 tsb", "name": "Salt" },
{ "amount": "1/2 tbsp", "name": "Ground pepper" },
{ "amount": "2 tbsp", "name": "Butter" }
],
"instructions": "Chop the onion finely. Put in a bucket. Press the garlic into the bucket. Add egg, bread crumbs, cream, beef stock, salt and pepper and mix. Let it rest for 10 mins. Mix in the minced meat. Make small balls and fry (8-10 at a time) in butter until golden brown and cooked through."
},
{
"name": "Kalops",
"description": "This Swedish stew should be eaten with potatoes and pickled red beets.",
"containsMeat": true,
"ingredients": [
{ "amount": "1 kg", "name": "Boneless Beef" },
{ "amount": "2 tbsp", "name": "Butter" },
{ "amount": "2", "name": "Yellow onions" },
{ "amount": "4", "name": "Carrots" },
{ "amount": "3", "name": "Bay leafs" },
{ "amount": "30", "name": "Allspice grains" },
{ "amount": "1 litre", "name": "Water" },
{ "amount": "2 tbsp", "name": "Concentrated beef stock" },
{ "amount": "1 1/2 tbsp", "name": "Corn starch" },
{ "amount": "1 tsb", "name": "Salt" },
{ "amount": "1/2 tbsp", "name": "Ground pepper" }
],
"instructions": "Cut the meat into 3x3 cm pieces. Peel and cut onions and carrots into 3cm pieces. Fry the meat in a pan with butter until it has a nice surface. Add onion, carrots, bay leafs and allspice. Pour over the water and beef stock. Bring to boil and leave until the meat is tender (2-3 hours depending on meet). Thicken the stew with the corn starch. Add salt and pepper."
}
]
```
Now we have some data to play with. Let's start by taking a look at it. We want to look at both the outer list (the recipes) and one of the inner lists (a specific) recipe, just to get a feel for it. Connect the **Items** output of the **Static Array** to an **Array Filter** node. Then take the **First Item Id** of the **Array Filter** and connect it to an **Object** node. Add a property to the **Object** node called `ingredients` and connect that one to an **Array** node. Also open up the debug popup on the connections to see what's going on.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/arrays/debug-1.png)
</div>
<div className="ndl-image-with-background xl">
![](/docs/guides/data/arrays/debug-2.png)
</div>
## Filtering out the first item in an **Array**
To explain in more on what's going on. We take the items from the **Static Array** and feed them into a **Filter Array** node. The filter actually doesn't have any filters enabled but we can make use of its **First Item Id** output which holds the Id of our first **Object** in the **Array** (the Pancake recipe). In that Object we look at the **ingredients** property which contains an **Array** of items. These items are set as **Items** for another **Array** which now will contain the ingredients of the Pancake recipe.
Phew! The **Array Filter** / **First Item Id** output is very useful if you want to get a sample of what's in a list.
Let's create some simple UI around our recipes. To make the app a little easier to interact with, we need a simple navigation system. You can learn more about how to do navigation in [this](/docs/guides/navigation/basic-navigation) guide. In this guide we will not go into details.
## Making a list to show the **Array**
Start by adding a **Page Router** in the root component. Also get rid of the **Text** node.
<div className="ndl-image-with-background m">
![](/docs/guides/data/arrays/page-router.png)
</div>
Click on the **Page Router** and add a new page called `Recipe List`.
<div className="ndl-image-with-background m">
![](/docs/guides/data/arrays/create-page-1.png)
</div>
You will now have a new **Page** component in your component tree.
<div className="ndl-image-with-background m">
![](/docs/guides/data/arrays/recipe-list.png)
</div>
Create yet another component. This time a visual component called `Recipe List Item`. This will be a list item for a list we soon are going to build.
<div className="ndl-image-with-background m">
![](/docs/guides/data/arrays/vis-comp.png)
![](/docs/guides/data/arrays/recipe-list-item.png)
</div>
In that component, set the size of the list item to be **width** 100% and **height** 30 px. Also add a [Text](/nodes/basic-elements/text) node to the root **Group**. Center the text vertically and left align it.
<div className="ndl-image-with-background m">
![](/docs/guides/data/arrays/recipe-list-item-2.png)
</div>
Now go to the newly created `Recipe List` page. Add a text node that will be the title. Maybe increase the font size to 24 px. Give the Page some white space by adding 10 px padding top and left direction.
<div className="ndl-image-with-background m">
![](/docs/guides/data/arrays/recipe-list-page-1.png)
</div>
Then add a [Repeater](/nodes/ui-controls/repeater) node under the title **Text** node. Make sure the newly created `Recipe List Item` is used as a template.
<div className="ndl-image-with-background m">
![](/docs/guides/data/arrays/repeater-item.png)
</div>
Now move (for example using cut/paste) the **Static Array** node from the main `App` to this page and connect the **Items** output to the **Repeater**.
<div className="ndl-image-with-background l">
![](/docs/guides/data/arrays/recipe-list-2.png)
</div>
The final step to get the list working is to connect the correct data point in the **Object** to the **Text** in the list item. Go to `Recipe List Item` and add a new **Object** node. Make sure its **Id** comes from the repeater.
<div className="ndl-image-with-background s">
![](/docs/guides/data/arrays/id-from-repeater.png)
</div>
Then add a property on the **Object** called `name` and connect it to the **Text** node.
<div className="ndl-image-with-background l">
![](/docs/guides/data/arrays/recipe-list-3.png)
</div>
This simple list will do for now. If you want to learn more about how to make lists it's recommended to go through the list guides, starting with [List Basics](/docs/guides/data/list-basics).
## Removing an Object from an **Array**
So let's add some functionality to the list. Let's make sure we can remove some recipes. We begin by adding an button for it on our list item. Go into the `Recipe List Item` component.
Change the layout of the root **Group** to `horizontal`. Then add a new **Group** as a sibling to the **Text** node and in that **Group** node an [Icon](/nodes/basic-elements/icon) node. Find a good icon for delete and also make sure it is black, so you can see it properly. Also, change the sizing of the newly added **Group** node to `Content Width and Height`. Tweak the size, margins and padding so it looks ok.
<div className="ndl-image-with-background l">
![](/docs/guides/data/arrays/recipe-list-item-3.png)
</div>
We only want the delete button to show up when you hover the list item. We achieve this by adding a [Switch](/nodes/logic/switch) and hooking it up to **Hover Start** and **Hover End** and feeding its state to the **Mounted** input of the inner **Group** see below.
<div className="ndl-image-with-background l">
![](/docs/guides/data/arrays/recipe-list-item-4.png)
</div>
Finally we want to send a signal out of the list when the user clicks the delete icon. So add a [Component Output](/nodes/component-utilities/component-outputs) node. Make and input called `Request Delete` and connect the **Click** signal from the inner **Group** to it.
<div className="ndl-image-with-background l">
![](/docs/guides/data/arrays/recipe-list-item-5.png)
</div>
We are now ready to go back to the `Recipe List` page component and deal with the actual deletion. In that component we first need to add an Array that will hold the recipe items. Before we fed the **Items** directly from the **Static Array**. Now, since we need to modify the contents, we need to first feed the **Items** into an **Array**. Give the new **Array** the name `Recipe List`. The node construct should now look like below.
<div className="ndl-image-with-background l">
![](/docs/guides/data/arrays/recipe-list-4.png)
</div>
Now we can do the removal. Create a **Remove Object from Array** node. This node takes two inputs. The **Array Id** and the **Object to Remove Id**. The **Array Id** is the same as the **Array** we just created (`Recipe List`) so let's just connect the two Ids. The **Object to Remove Id** comes from the repeater. The **Id** of the object that was clicked will forwarded by the **Repeater**. Also, our output from the `Recipe List Item` component will come out of the repeater as a signal and we can use that as the signal for triggering the delete by connecting it to the **Do** signal of the **Remove Object From Array** node.
<div className="ndl-image-with-background l">
![](/docs/guides/data/arrays/recipe-list-5.png)
</div>
Try it out! Don't be afraid to delete recipies. If you refresh your viewer they will all be back since they are not coming from a database but from a **Static Array**.
## Using **Count**
If you remove all recipes, it might be a good idea to show a text, `The recipe list is empty`. Lets build that! Start by adding a **Text** node to the `Recipe List` page. Center the text (using the `Text Alignment` property).
We only want to show this text when the **Array** is empty. So add an [Expression](/nodes/math/expression) node. Type the following into the expression node
```javascript
count === 0
```
This will make the expression return `true` if the input `count` equals 0, otherwise `false`. Connect the output **Result** of the expression to the **Mounted** input of the new **Text** node. Then finally connect the **Count** output from the **Array** to the `count` input of the expression.
<div className="ndl-image-with-background l">
![](/docs/guides/data/arrays/recipe-list-6.png)
</div>
Try removing all recipes and make sure the text is visible once **Count** equals 0.
## Showing the recipe
Ok, now it's time to show the recipes once you click on them. So let's start by adding another page. As before, click the **Page Router** and then click **Add Page**. Add a page name `Recipe`.
In the new **Page** start by adding a title text. We can copy the **Text** node that was used as a title in the `Recipe List`. Below that we want another **Text** node which will hold the description. Let's add that as well.
<div className="ndl-image-with-background m">
![](/docs/guides/data/arrays/recipe-1.png)
</div>
### Passing on the Recipe to the **Page**
We now need to pass on the **Id** of the **Recipe** we clicked to the new **Page** so we can show it. Step 1 is to catch when we click the recipe. Remember, we only catch the case when we click the delete button so far. So go back to the `Recipe List Item`. Add another output on the **Component Outputs** node called `Request View`. Then connect the **Click** output on the root **Text** node to it. This will forward the signal to the component containing the list.
<div className="ndl-image-with-background l">
![](/docs/guides/data/arrays/recipe-list-item-6.png)
</div>
Now go back to the `Recipe` Page. We want to add a new **Path Parameter** in the **Page Inputs** component. Click the **Page Inputs** node and then add a new **Path Parameter** called `RecipeId`. This will mean that we can send the **Id** of the clicked recipe is part of the url path that we navigate to.
<div className="ndl-image-with-background">
![](/docs/guides/data/arrays/page-param-1.png)
</div>
Assuming that the **Id** is this **Path Parameter**, we can connect the **Object** to it and get the name, description and other properties from the **Object**. Let's set that up. We are still in the `Recipe` page.
<div className="ndl-image-with-background">
![](/docs/guides/data/arrays/recipe-2.png)
</div>
Now we are ready to implemnt the navigation. In the `Recipe List` page we add a [Navigate](/nodes/navigation/navigate) node. Select the `Recipe` page to navigate to. This will add an input to the **Navigate** node which is our **Path Parameter** `RecipeId`. Connect the **Item Id** output of the **Repeater** to it and finally the **Request View** signal to the **Do** signal of the **Navigate** node. Voila!
<div className="ndl-image-with-background l">
![](/docs/guides/data/arrays/recipe-list-7.png)
</div>
Note that there is no Back-button in the Noodl viewer so you either have to use your browser if you want to go back and forth, or simply refresh to get back to the list. That's a little bit annoying but we'll fix it later. We want to show our ingredients first.
## Showing the recipe ingredients
So let's build the list that shows our ingredients. Create a new list item, a visual component named `Ingredient Item`. We can actually duplicate our previous list item and modify it.
<div className="ndl-image-with-background m">
![](/docs/guides/data/arrays/duplicate.png)
</div>
Start with removing the `Request View` input in the **Component Output** since we are not going to view individual ingredient. Also remove the delete action and the hover state. Looking at the data, the ingredient have two properties. `name`, which we already have connected. The other one is `amount`. Let's add a text node for that and connect it.
<div className="ndl-image-with-background m">
![](/docs/guides/data/arrays/ingredient-list-item-1.png)
</div>
Now we can add the **Repeater** node that will display the ingredients. Go to the `Recipe` Page. Add a **Repeater** node and select the newly created `Ingredient List Item` as the component template. Now we need to find the list of ingredients. Looking at the data model we see that the property is called `ingredients`. Add that property to the **Object**. Then add an **Array** node. That's the **Array** that will hold the ingredients. Connect the `ingredients` property of the **Object** to the **Array**. Then connect the **Items** output of the **Array** to the **Repeater**. You now have a list of ingredients!
<div className="ndl-image-with-background xl">
![](/docs/guides/data/arrays/recipe-3.png)
</div>
We might as well add the instructions at the end to make the view complete. Add another **Text** node at the bottom. Call it `Instructions`. Then add the property `instructions` on the **Object** and connect it to the **Text** node. Maybe add some margin to seperate it a bit from the list.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/arrays/recipe-4.png)
</div>
Finally, let's add that back button. At the bottom, add a **Button** that triggers a navigation to the `Recipe List` **Page**. Connect the button to a **Navigate** node that takes the user back to `Recipe List` **Page**.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/arrays/recipe-5.png)
</div>
## Feeding a Dropdown with an **Array**
We are making progress with our recipe App. Next step is to add a simple filtering functionality to filter out vegetarian / non vegetarian options. Remember, in our data model we have the property `containsMeat` that is either `true` or `false`.
We go back our our `Recipe List` page. In here we want to add a [Dropdown](/nodes/ui-controls/dropdown) with three options `all`,`meat` or `vegetarian`.
If you read up on the **Dropdown** node, you can see that to figure out its options it takes items from an **Array** as an input. The **Array** should contain items of the following type:
```json
{
"Label": "a label",
"Value": "a value"
}
```
The `Label` will be shown as a text in the **Dropdown** and the `Value` is the value that the **Dropdown** will have on its output if the associated item is selected.
We can easily feed the **Dropdown** using a **Static Array**.
Start by adding a **Static Array** in the `Recipe List` page and make sure it's set to JSON format. Name it `Dropdown Options` for clarity and add in the following data
```json
[
{ "Label": "Show all recepies", "Value": "all" },
{ "Label": "Show recepies with meat", "Value": "meat" },
{ "Label": "Show vegetarian recepies", "Value": "veg" }
]
```
<div className="ndl-image-with-background xl">
![](/docs/guides/data/arrays/dropdown-options.png)
</div>
Then add in a **Dropdown** node, just after the title. Enable the `label` on it and give it the label `Filter`. Also give it a bottom margin to seperate it a little from the list items. Then connect the **Items** from the **Static Array** to the Dropdown and you should see the options in the **Dropdown**.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/arrays/recipe-list-8.png)
</div>
## Filtering an **Array**
We can now proceed with the filtering of the recipe. In our case we have three filtering states:
- No filtering, i.e. we show all recipes
- Show only the meat recipes, i.e. `containsMeat == true`
- Show only the vegetarian recipes, i.e. `containsMeat == false`
We can encode these in a [States](/nodes/utilities/logic/states) node. Add in a **States** node. Give it a name, `Filter States`. Then create three states. To match up with our dropdown values we will call them `all`, `meat` and `veg`. By calling them exactly what the `Value` of the dropdown will give us will make it possible to let the **Dropdown** set the state without any intermediary logic.
Then create two **Values** for the **States** node. Call one, `Filter Enabled`, one `containsMeat`. Both should be of the type `Boolean` We will now assign these **Values** for each state.
- `all` - `Filter Enabled` should be `false`, `containsMeat` can be either `true` or `false` (doesn't matter).
- `meat` - `Filter Enabled` should be `true`, `containsMeat` should also be `true`.
- `veg` - `Filter Enabled` should be `true`, `containsMeat` should be `false`.
Now add in a **Array Filter** node. This should be in between the **Static Array** that contains the recipes and the **Recipe List** that holds the items to be presented in the list.
Create a new filter for the property `containsMeat`.
<div className="ndl-image-with-background m">
![](/docs/guides/data/arrays/array-filter-1.png)
</div>
Set the filter to be of `Boolean` type. We now are able to control this filter through an input, i.e. weather the filter should filter for `containsMeat == false` or `containsMeat == true`.
<div className="ndl-image-with-background s">
![](/docs/guides/data/arrays/array-filter-2.png)
</div>
So hook up the **Items** from the **Static Array** containing the recipes to the **Items** of the **Array Filter**. Then from the **Array Filter** to the **Array**.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/arrays/recipe-list-9.png)
</div>
Now we need to control the filter depending on state. So first connect **Value** from the **Dropdown** to our states node. Changing the **Dropdown** will now change the state of the **States** node. Then we connect the **Values** of the **States** node to the inputs on the **Array Filter**, **Enabled** and `containsMeat Filter Value`.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/arrays/recipe-list-10.png)
</div>
You can now filter the recipes.
<ReactPlayer playing autoplay muted loop url="arrays/recipe-list-1.mp4" />
## Creating New Recipes
### Make a simple form
As a final feature we also want to add in the option to create new recipes. It will involve two things:
- Create a new recipe, including data for the recipe, for example, name and description
- Create a new ingredients array for the new recipe and fill with ingredients
Let's start by creating some UI for this functionality.
Start by adding a [Button](/nodes/ui-controls/button) at the bottom of the `Recipe List` page. Give it the label `Add new recipe`. Center it and give it some margin.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/arrays/recipe-list-11.png)
</div>
Clicking this **Button** should take us to a new **Page** where we can add in the information about the recipe. So let's create that page. Find the **Page Router** in the main App and click **Add new page**. Call the new **Page** `New Recipe`. Open the new **Page** component. Copy and paste a title **Text** node from the `Recipe List` page and change the title to `New Recipe`.
Add a new **Navigate** node in the `Recipe List` **Page** so we can get to our new **Page**.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/arrays/recipe-list-12.png)
</div>
In our new form we need controls to store the following in our new recipe.
- A [Checkbox](/nodes/ui-controls/checkbox) to store the `containsMeat` value
- A [Text Input](/nodes/ui-controls/text-input) to store the `name` of the recipe
- A **Text Input** to store the `description`
- A **Text Input** to store the `instructions`
- Two **Buttons**, one for Save and one for Cancel
Add those controls to the new **Page** and make sure the labels are properly set to describe what the user should fill out in each control. Tweak padding, margins and layout to make it look ok. Set the `description` and `instructions` **Text Inputs** as **Text Areas**.
<div className="ndl-image-with-background l">
![](/docs/guides/data/arrays/new-recipe-1.png)
</div>
### Hooking up the data
Looking at the **Page** from a data perspective, this is what we need to do:
1. Once we enter the new page, we should create a new **Object** that will hold the recipe data.
2. After creating the **Object** we also need to create a new **Array** that will hold the ingredients. The items of this **Array** should later be stored in the **Object**.
3. If the user clicks `Save` we should add this **Object** as a new item in our recipe **Array**.
## Creating an **Array** dynamically
Let's orchestrate this based on the **Did Mount** event of the **Page**.
Create a **Create New Object** node. Add the property `containsMeat` and make sure it's set to `false` by default to make sure our filter works. Trigger the **Create New Object** node on **Did Mount**. Once the **Object** is created, we should create the **Array** for the ingredients. When the user clicks `Save` we will store the items in the `ingredients` **Array** in the **Object** representing the recipe.
Then create an **Object** and hook up the controls (**Checkbox** and **Text Inputs**) to the properties they are associated with. Note that we are not using a **Set Object Properties** because we want the **Object** to be updated at any change on the inputs, i.e. not at a specific time.
Here's how the flow looks. Note that it's important to use the **Done** signals to make things happen sequentially, as well as passing on the **Id** of the newly created **Object** to the **Object** node to make it act on the correct **Object**.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/arrays/new-recipe-2.png)
</div>
### Add ingredients to the **Array**
Now we need to add some UI to make it possible to add ingredients. The idea is to have two **Text Inputs** (one for amount and one for the name of the ingredient) and a **Button** that adds the ingredient. We also need a **Repeater** that shows the ingredients as you add them. That repeater can reuse our previous `Ingredient List Item` component. Of course, the **Repeater** gets its items from the **Array** we created that holds the ingredients. Let's also add in a **Text** as a title for the ingredients section to make it easier to understand the form.
All the components are put in a **Group** called `Ingredient Section` to make them easy to handle as a compound.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/arrays/new-recipe-3.png)
</div>
## Adding **Objects** to an **Array**
Now we need to create a new **Object** (representing the ingredient) when the user clicks `Add` and then add that new **Object** to the **Array** of ingredients. We use the node **Insert Object Into Array** for the latter. It takes one **Id** for the **Array** to add to and one **Id** for the **Object** to add. We hook everything up as below.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/arrays/add-ingredient-1.png)
</div>
Try adding a few ingredients. They should show up in the ingredient list.
### Adding the new recipe to the Recipe **Array**
Finally, when the user presses `Save` we want to add the ingredients to the recipe and then add the new recipe to our main recipe **Array** - the one we create in the `Recipe List` **Page** that has an **Id** `Recipe List`.
First we store the **Items** of our ingredients in the `ingredients` property using the **Set Object Properties**. Then, once again we use the **Insert Object Into Array** node to save our recipe in the recipe list. This time we hardcode the **Id** to `Recipe List` and make sure our recipe **Object** is the object to add.
:::note
Another option would have been to store the **Id** of the **Array** instead of its items and then use the **Id** to identify the **Array** when showing the recipe. However, since the **Static Array** don't have an internal **Array** holding its ingredients, this would not work in this case. That's why we store the **Items** rather than the **Id**.
:::
We also make sure the user navigates back to the `Recipe List` once the recipe is added. Same goes for if the user presses **Cancel**, but of course in that case, nothing is saved.
This is how that part is done.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/arrays/save-recipe.png)
</div>
### A minor refactoring
We now expect our newly created recipes to be visible in the Recipe List. However, we can see that's not the case. It's the same old list. What's going on? The reason is that since the `Recipe List` **Page** is created as we nevigate, the items are overwritten with the items of our **Static Array**. See image below.
<div className="ndl-image-with-background l">
![](/docs/guides/data/arrays/static-array-2.png)
</div>
We also see that the filtering happens at the wrong place - we are only filtering what's coming out of the **Static Array**.
To solve this we need to move the assignments of items out, outside the **Page** (that is being re-created) to the main App that's not being recreated when a navigation happens. This is easy since the **Array** can easily be referred to globally through its **Id**.
We simply reorganize our **Arrays** and filters according to the following. We move the **Static Array** and assignment out to the App as below.
<div className="ndl-image-with-background m">
![](/docs/guides/data/arrays/assignment.png)
</div>
And we use the same **Array** (identified by the **Id**) and make sure to filter the items before we feed them into our **Repeater**
<div className="ndl-image-with-background xl">
![](/docs/guides/data/arrays/recipe-list-13.png)
</div>
Try it out, now it should work!
<ReactPlayer playing autoplay muted loop url="arrays/final-1.mp4" />
<div className="ndl-image-with-background m">
![](/docs/guides/data/arrays/thumbnail-1.png)
<ImportButton zip="/docs/guides/data/arrays/arrays.zip" name="Recipe App" thumb="/docs/guides/data/arrays/thumbnail-1.png" />
</div>

View File

@@ -0,0 +1,263 @@
---
title: External Data
hide_title: true
---
import useBaseUrl from '@docusaurus/useBaseUrl'
import ImportButton from '/src/components/importbutton'
# External Data
## What you will learn in this guide
So far we have been working exclusively with local data, often coming from a **Static Array**. This is great for building prototypes and getting started quickly but in real life we will need to integrate with external data. This guide shows you how to do that using REST APIs, the most common type of APIs for web applications.
## Airtable
For this guide we are going to use the [Airtable](https://airtable.com) REST API to read and write to an **Airtable** table. This guide assumes that you know a little bit about REST APIs, and to follow along you need to create an account for **Airtable**. You can read more about the **Airtable** REST API [here](https://support.airtable.com/hc/en-us/articles/203313985-Public-REST-API). Most REST APIs work in similar ways so the knowledge gained here should be applicable on most other APIs.
In this guide we are going to extend the CRUD example from the [UI Controls and data](/docs/guides/data/ui-controls-and-data) guide by connecting it to an **Airtable** table to create, update, read and delete records. To do this I have create a new airtable base and added a table called **Members**, I have also created three fields corresponding to the member object of the guide.
* **Full Name**, a string that contain the name of the member.
* **Receive Marketing Emails**, a checkbox (boolean) that indicated if the member should receive marketing emails
* **Awesomeness**, a number between 0 and 100 indicating the general awesomeness of the member
<div className="ndl-image-with-background l">
![](/docs/guides/data/external-data/airtable.png)
</div>
There is a nice documentation for the different **Airtable** REST API operations that you can perform on a table, but we will review them briefly here before we use them.
Let's get started. We are going to start where we left off in the [UI Controls and data](/docs/guides/data/ui-controls-and-data) guide, if you don't have ready you can import the finished components here.
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/data/external-data/final-crud.mp4")}/>
<ImportButton zip="/docs/guides/data/external-data/final-crud-1.zip" name="CRUD Example" thumb="/docs/guides/data/external-data/final-crud-thumb.png" />
</div>
## The REST node
To do REST API calls we are going to use the built in [REST](/nodes/data/rest) node. First the let's go over some of the properties that are important to know about:
<div className="ndl-image-with-background l">
![](/docs/guides/data/external-data/rest-props.png)
</div>
* **Resources** this is the REST API endpoint and resource, i.e. the URI that will be requested.
* **Method** is the HTTP method that will be used, it can be any of **GET**, **POST**, **PUT**, **DELETE**, **PATCH**
Once you have specified the resource as well as the method there are two small scripts that you will need to use to the first script prepares the request before it is sent, and the second script parses the response.
<div className="ndl-image-with-background l">
![](/docs/guides/data/external-data/rest-props-2.png)
</div>
First, let's take a look at the request script. Using this script you will prepare the request, this is done be modifying the **Request** object. You can change the **resource** (the URI) and the **method** from before.
```javascript
Request.resource = "https://example.org"
Request.method = "POST"
```
You can also modify the HTTP headers.
```javascript
Request.headers['authorization'] = "shhh it's a secret"
```
The ```Reuqest.parameters``` object can be used to set query parameters for the URI. The script below would add ```?search=find-this``` to the URI before the request is made.
```javascript
Request.parameters['search'] = "find-this"
```
Finally you can modify the ```Reuqest.content``` object. This should be a JSON compatible object that is sent if the method is **PUT**, **POST** or **PATCH**. As in many scripts in Noodl you can use the ```Inputs``` object to specify any custom inputs you would like for the node.
```javascript
Request.content = {
something:Inputs.MyInput
}
```
The script above will create a custom input called **MyInput** on the Rest node. Any value connected to that input will be available in the request script using ```Inputs.MyInput``` and in the script above it is used as part of the request content.
Second, we have the **response** script. This script will take the reponse from the REST API call (if it is successful), parse it and expose it as custom outputs on the **REST** node. You have two important objects ```Response.status``` which will contain the HTTP status of the response, and ```Response.content``` which will contain the content parsed as JSON. Just like the request script used the ```Inputs``` object to create custom inputs, this script can also use the ```Ouputs``` object to create custom outputs to expose the response. Let's take a look at a way to use this with the **Airtable** API.
This is a brief introduction to how the REST node works. You can read more in the reference documentation for the [REST](/nodes/data/rest) node. Now let's continue with connecting the CRUD example to **Airtable** using the **REST** node.
## Listing all members
First we're going to use a **REST** node to list all members from our **Airtable** table and put in the **Array** node we use to store members. To do this we need to perform the following REST API call (you need to replace the **id** of the **Airtable** base with your own, and make sure your table is called **Members**).
```javascript
GET https://api.airtable.com/v0/appGx6whFNzxu54eP/Members
```
We will simplye create a **REST** node and change the **Resource** property to the URI above (with your own base id). Then we will connect the **Fetch** input of the **REST** node to the **Did mount** signal output of the root **Group** node. This will make sure that we perform the request as soon as the **Group** node becomes visible. I gave the node a new label "List all members", just to keep track of what it is doing.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/external-data/list-all-members.png)
</div>
We also need to modify the **Request** script to provide the **Airtable API Token**, otherwise the requrest will not go through. Change the request script of the **REST** node to the following, providing your own **API token**.
```
Request.headers['Authorization'] = 'Bearer your-api-token'
```
Now the request should go through if you refresh the viewer in the Noodl editor so that the **Did mount** signal is sent. A sample response from **Airtable** will look something like below. We are going to need to parse this response for it to be useful in our app.
```
{
"records": [
{
"id": "rec0Qw9FdqjcaDF2W",
"createdTime": "2022-04-14T08:40:28.000Z",
"fields": {
"Full Name": "Lisa Simpson",
"Awesomeness": 80
}
},
{
"id": "recMbwuixscqMroQn",
"createdTime": "2022-04-14T14:26:42.000Z",
"fields": {
"Full Name": "Marge Simpson",
"Awesomeness": 0
}
},
{
"id": "recScNKnPqO3xHd4c",
"createdTime": "2022-04-14T08:40:28.000Z",
"fields": {
"Receive Marketing Emails": true,
"Full Name": "Homer Simpson",
"Awesomeness": 50
}
}
]
}
```
We will do that in the response script of the **REST** node. We will need to create a simple array with objects with the **id** and fields directly as properties, then we will return it as an output of the **REST** node. The following small script will do the trick. This should go into the **Response** script.
```
Outputs.Members = Response.content.records.map((r) => ({
id:r.id,
'Full Name':r.fields['Full Name'],
'Receive Marketing Emails':r.fields['Receive Marketing Emails'] == true,
'Awesomeness':r.fields['Awesomeness']
}))
```
We use the ```Outputs``` object to specify a new custom output on the **REST** node that will contain an array of all member objects. It's important that we keep the **id** (it should be lowercase) as it will become the **id** of our **Object**s and we need to use it later. With that script in place, we will have a new output on our **REST** node that we can connect to the **Array** where we keep our members.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/external-data/list-all-members-2.png)
</div>
That's great, now we can list all members and show in the user interface. With that in place, let's move onto to editing and deleting.
## Creating a new member
Now we will repeat the same process for doing a REST API call to create a new member in the **Airtable** table. For that we will use a **REST** node and provide the following resources and method.
```javascript
POST https://api.airtable.com/v0/appGx6whFNzxu54eP/Members
```
Here we need to modify the request script to take an **Object Id** as input and put the properties of that object in the ```Request.content``` object. The following script will do the trick:
```
Request.headers['Authorization'] = 'Bearer your-api-key'
const obj = Noodl.Object.get(Inputs.ObjectId)
Request.content = {
records: [
{
fields: {
"Full Name": obj["Full Name"],
"Receive Marketing Emails": obj["Receive Marketing Emails"],
"Awesomeness": obj["Awesomeness"]
}
}
]
}
```
First set the **Authorization** header with the **API token** just as before. Then we use a custom input ```Inputs.ObjectId``` that will contain the **Id** of the Noodl object we want to send as part of the REST API call. We use the ```Noodl.Object.get``` function to get the object from it's **Id**. With the object at hand we can format the content in a way that the **Airtable** API is expecting. With the script above in place, we can hook the **REST** node up as follows.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/external-data/create-new-member.png)
</div>
This is, when the newly created member object have been added to the **Array** we will trigger the **REST** request by connecting the **Done** output signal from the **Insert Object Into Array** node to the **Fetch** input on the **REST** node. We will also make sure to connect the **Id** of the newly created object to the custom **ObjectId** input that we are using in our request script in the **REST** node.
:::note
One small caveat, when you create a new object in Noodl using the **Create New Object** node it will be assigned a new random **Id** (you can see it by inspecting the connection). But when we create the member in **Airtable** using the request above it will get a new **Airtable** internal id. We need this to perform edits and deletes. The simplest way to solve this is to issue a new "list all members" request. Simply connect the **Success** signal from the "Create new member" REST node to the **Fetch** input of the "List all members" REST node.
:::
Sweet, now we can list members, we can look at the details of a member, and we can even create new members. Give it a try, you should see the new member in the **Airtable** table. Now let's move on to editing a member.
## Editing a member
Editing a member is very similair to creating a new member. We need to use a different method but the same resource. We also need to provide the **Object Id** as part of the URI. As this is a very common pattern for REST APIs there is a neat trick. You can specify a custom input directly in the URI using the ```{MyInput}``` notation. So we will use the following resource and method on this **REST** node. This will automatically create the correct URI for us if we provide the **ObjectId** as input.
```javascript
PATCH https://api.airtable.com/v0/appGx6whFNzxu54eP/Members/{ObjectId}
```
We still need to format the ```Request.content``` object in a way that the **Airtable** API can consume. This is done with the following request script, very similair to the script we used when creating new members.
```
Request.headers['Authorization'] = 'Bearer your-api-token'
const obj = Noodl.Object.get(Inputs.ObjectId)
Request.content = {
fields: {
"Full Name": obj["Full Name"],
"Receive Marketing Emails": obj["Receive Marketing Emails"],
"Awesomeness": obj["Awesomeness"]
}
}
```
With that in place we can hook it up to the output signal **Save** that we send from the edit form component. We also need the **Object Id** as before, and this we can get from the **Edit** event, that is the **Receive Event** node. That is the same **Object Id** that we pass to the edit form component when it is shown. It will look something like this:
<div className="ndl-image-with-background xl">
![](/docs/guides/data/external-data/edit-member.png)
</div>
## Deleting a member
And finally, we need to add support for deleting a member. This we will do, very similair to before, but this time in the **Member Item** component. For that we will use the following resource and method.
```javascript
DELETE https://api.airtable.com/v0/appGx6whFNzxu54eP/Members/{ObjectId}
```
The request script simply needs the API access token.
```
Request.headers['Authorization'] = 'Bearer your-api-token'
```
And then we can hook it up as shown below in the **Member Item** component.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/external-data/delete-member.png)
</div>
That's it. This guide has shown how to use the **REST** node to hook up our CRUD example from the previous [guide](/docs/guides/data/ui-controls-and-data) to and external REST API. With the **REST** node you can connect to most types of APIs that use JSON to send and receive data.

View File

@@ -0,0 +1,283 @@
---
title: List Basics
hide_title: true
---
import ImportButton from '../../../src/components/importbutton'
# List Basics
## What you will learn in this guide
This guide will teach you how to create basic lists in Noodl using the [Repeater](/nodes/ui-controls/repeater) node that is being fed through an [Array](/nodes/data/array/array-node). It will show how to create a simple list item and how to detect that the user clicks it.
## Overview
The guide will walk you through the following steps.
- The **Repeater** node
- Creating a simple **List Item** component
- Connecting the data to the **List Item**
- Detecting when the user clicks an item
## The Repeater Node
The main node for making lists is the **Repeater** node. The **Repeater** node takes an **Array** as an input (the **Items** property). It then and creates a copy of the provided **List Item** template as a component and connects the specific **Object** (or **Record**) in the **Array** with its respective **List Item**.
The **Array** could for example be coming from an [Array](/nodes/data/array/array-node) node, from a database through a [Query Records](/nodes/data/cloud-data/query-records) or a [Static Array](/nodes/data/array/static-array).
Let's get started by starting a new project using the "Hello World" template. Remove the **Text** node and add in a **Repeater** node instead.
<div className="ndl-image-with-background">
![](/docs/guides/data/list-basics/repeater-1.png)
</div>
The **Repeater** needs two things to work:
1. Some **Array** data that can be represented as _List Items_
2. A _Template component_ that will be repeated per item in the **Array**
We will start with the **Array**. In this case we will use the **Static Array** might as well be some other node that outputs an **Array** for example a [Query Records](/nodes/data/cloud-data/query-records) node that queries records from a database. There is a specific guide for working with **Query Records** [here](/docs/guides/cloud-data/quering-records-from-database).
## Providing Items
So create a **Static Array** node. Change its type to `JSON` and paste in the following data.
```json
[
{
"name": "Peter Reid",
"age": 29,
"team": "Everton",
"position": "Midfield"
},
{
"name": "Chris Waddle",
"age": 25,
"team": "Tottenham",
"position": "Midfield"
},
{
"name": "Mark Hateley",
"age": 24,
"team": "Milan",
"position": "Forward"
},
{
"name": "Peter Beardsley",
"age": 25,
"team": "Newcastle",
"position": "Forward"
},
{
"name": "Kenneth Sansom",
"age": 27,
"team": "Arsenal",
"position": "Defender"
}
]
```
<div className="ndl-image-with-background l">
![](/docs/guides/data/list-basics/static-array-1.png)
</div>
Connect the **Items** output of the **Static Array** to the **Items** input of the **Repeater** node.
<div className="ndl-image-with-background l">
![](/docs/guides/data/list-basics/repeater-2.png)
</div>
Now we have items flowing into the repeater.
## Create a List Item Template
Next step is to create a component that can serve as a template for the **Repeater**. One component will be created per item in the **Array** that's feeding the **Repeater**.
Create a new Visual Component. Call it "Player Item".
<div className="ndl-image-with-background">
![](/docs/guides/data/list-basics/list-item-1.png)
</div>
<div className="ndl-image-with-background">
![](/docs/guides/data/list-basics/list-item-2.png)
</div>
In the new component we will add in three [Text](/nodes/basic-elements/text) nodes that will present the information about each player. Make sure to change the layout of the root node to `Horizontal` and add in the three **Text** nodes. Call the **Text** nodes, `name`, `age`, `team` so we can keep track of them. Note that you can use the Canvas view to get a first view of how your list item will look, even before using it in your **Repeater**.
<div className="ndl-image-with-background">
![](/docs/guides/data/list-basics/list-item-3.png)
</div>
## Set the List Item Template
Go back to the main component. We now need select our newly created component as our template. Click the **Repeater** and set the template to "Player Item".
<div className="ndl-image-with-background">
![](/docs/guides/data/list-basics/repeater-3.png)
</div>
You should now see the first view of you data and list item, namely something like the screen below.
<div className="ndl-image-with-background">
![](/docs/guides/data/list-basics/result-1.png)
</div>
The repeater have created an instance of the "Player Item" component for each item in the incoming array. But we immediately see two things to fix:
1. The layout is weird. The **Repeater** takes up the full screen. It then divides the space equally among each list item. We should probably fix the layouting of our listitem a bit.
2. There is no data coming in from the list item. Our **Text** nodes are not connected to anything.
## Fixing the layout of the List Item
We deal with the layout first. In the "Player Item" component, click on the root **Group** and change the sizing to "Explicit width & content height". This will make the vertical size of the list item be as large as the **Texts** they contain, while still taking upp all horizontal space.
<div className="ndl-image-with-background">
![](/docs/guides/data/list-basics/list-item-layout-1.png)
</div>
Lets also add a border around each item and some rounded corners. Change "Border Style" and "Border Radius" to your liking. Finally add in some padding in all four direction (to give some air within the list item) and a margin below the list item to get some air between items.
<div className="ndl-image-with-background">
![](/docs/guides/data/list-basics/list-item-layout-3.png)
</div>
<div className="ndl-image-with-background">
![](/docs/guides/data/list-basics/list-item-layout-2.png)
</div>
This looks better even if much more can be done. It will have to do for now.
## Connecting List Items to Data
Now we want to get the data from each item into the List Item. In the "Player Item" component, add in an [Object](/nodes/data/object/object-node) node. The **Object** node is one of the central data holding nodes in Noodl. Read more about them in a dedicated guide [here](/docs/guides/data/objects).
Click the **Object** and set the **"Get Id From"** property to `From Repeater`.
<div className="ndl-image-with-background">
![](/docs/guides/data/list-basics/object-1.png)
</div>
You have now told the **Object** that the **Repeater** will assign its **Id**, meaning each list item will have its **Object** node point to the respective **Object** in the **Array** that feeds the repeater.
If you hover over your newly created **Object** you will actually see that it contains the data of our last item in the **Array**.
<div className="ndl-image-with-background">
![](/docs/guides/data/list-basics/object-2.png)
</div>
?> Note that since there are several instances of the same List Item in memory at the same time you have to be a bit careful when reading the debug output in the node graph. Generally _the last value_ in a connection or component is going to be shown as debug data. In our case, the last list item data ("Kenneth Sansom") is shown even though all the other **Objects** in the **Array** is in memory as well.
Now we are ready to hook up the **Text** nodes to our data. Create three properties on the **Object** matching the ones we have in the static array, namely `name`, `age` and `team`.
<div className="ndl-image-with-background">
![](/docs/guides/data/list-basics/object-3.png)
</div>
Connect the **Object** properties to the **Text** input on the respective **Text** nodes.
<div className="ndl-image-with-background l">
![](/docs/guides/data/list-basics/connect-1.png)
</div>
While you are connecting the data to the **Text** nodes you should see the **List Items** being populated.
<div className="ndl-image-with-background l">
![](/docs/guides/data/list-basics/result-2.png)
</div>
## Detecting which List Item that was clicked
Finally, we also want to know when the user clicks an item and of course which item it was. Of course we could handle all business logic related to a **List Item** within it, but it's often better to let the **List Items** be pretty simple and handle any complex logic outside the **Repeater**. In short, we want the **Repeater** to be able to report when an item was clicked and which item was clicked.
You do this by letting the List Item component having [Component Outputs](/nodes/component-utilities/component-outputs) that are signals. You can have any number of signals coming out of your **List Item**. For example you may have a complex structure with buttons for changing and removing List Items and you want to know which one the user clicked. However in our simple case we only want to know if the **List Item** was clicked.
Go into the "Player Item" component. Add a **Component Outputs** node. In it, add a property "Click". Then connect the "Click" signal from the root **Group** to the newly created "Click" property on the **Component Outputs**.
<div className="ndl-image-with-background l">
![](/docs/guides/data/list-basics/list-item-4.png)
</div>
Now we are letting the **Repeater** know whenever a **List Item** is clicked.
Go back to the main app. We want to capture the outgoing **Click** signal together with the **Id** of the **Object** associated with the **List Item** that was clicked.
So create an **Object** node and connect the **Item Id** output that is now available on the **Repeater** to its **Id**.
<div className="ndl-image-with-background l">
![](/docs/guides/data/list-basics/connect-3.png)
</div>
This **Object** now will now point to the clicked item. To show that we can add a new **Text** node below the **Repeater**. Align it to the bottom and center and make sure its size is decided by its content, i.e. the **Text** node will only take up as much space as it needs, and it will end up in the center. Also make it larger, e.g. font size 24.
<div className="ndl-image-with-background">
![](/docs/guides/data/list-basics/text-props.png)
</div>
<div className="ndl-image-with-background l">
![](/docs/guides/data/list-basics/repeater-4.png)
</div>
Add the property **name** on the **Object** and connect it to the **Text** node.
We are almost done, we actually need to capture the **Click** event from the **Repeater** for the **Item Id** to update when the user clicks. We anyway only want to show the text for a short while when the user clicks.
To achieve this we add in a [Switch](/nodes/logic/switch) node. This will keep the state weather the **Text** is shown or not so call it "Selected Text Visible". Connect the **Current State** to the **Mounted** attribute of the **Text**. Then add a **Delay** node. Set the duration to 2000 milli seconds (2 seconds).
Let the **Clicked** signal coming from the **Repeater** start the **Delay** by connecting it to **Restart**. As soon as the **Delay** is started we want to show the **Text** so connect **Started** to **On** on the **Switch** and the **Finished** signal to **Off**.
<div className="ndl-image-with-background l">
![](/docs/guides/data/list-basics/repeater-5.png)
</div>
If everything works as expected you will now see the name of the player you click show up for two seconds at the bottom of the screen.
To import the project from the guide click "Import" on the image below.
<div className="ndl-image-with-background">
![](/docs/guides/data/list-basics/final-1.png)
<ImportButton zip="/docs/guides/data/list-basics/list-part-1.zip" name="List Basics" thumb="/docs/guides/data/list-basics/final-1.png" />
</div>

View File

@@ -0,0 +1,59 @@
---
title: Making Connections
hide_title: true
---
import useBaseUrl from '@docusaurus/useBaseUrl'
# Making Connections
## What you will learn in this guide
How to make data connections between nodes to start creating data driven user interfaces.
## Data connections
All nodes have inputs and outputs. Most of the properties of a node (that you can edit in the property panel) are also available as inputs. Many nodes also have outputs that provide some sort of data value. The most common example is the **Text Input** node that provides (amonng other things) the typed text as an output value. Using connections you can ensure that an output from one node is written to an input of another node when it is updated. Take a look at the very simple interface below:
<div className="ndl-image-with-background xl">
![](/docs/guides/data/making-connections/making-connections-ui.png)
</div>
Here you have a **Text Input** and a **Text** UI Element. Now let's say we want the **Text** element to show whatever we type into the **Text Input**. For that we can connect the two nodes.
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/data/making-connections/making-connection.mp4")}/>
</div>
Now when we type something in the **Text Input**, it will output that on the **Text** output. This is connected to the apptly named **Text** input of the **Text** node.
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/data/making-connections/testing-connection.mp4")}/>
</div>
As you can see above, when you start typing in the **Text Input** you can see the **Text** is also updated. You can also see in the node graph editor that the connection lights up briefly when data is updated on the output and written to the input.
If you hover the connection in the node graph editor you can also view the latest value that have been sent over the connection, and if you click the little inspection popup you can pin it. Click it again to unpin it.
Making direct connections is fun an all, but using the many utility nodes available you can convert and augment the data on it's way from the output to the final input. First, delete the connection you just made. Deleting a connection is done by clicking it once to reveal the **Delete Icon** and clicking it again to delete.
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/data/making-connections/delete-connection.mp4")}/>
</div>
Now, here you can see how we use a **String Format** node to make a nice greeting.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/making-connections/string-format.png)
</div>
Now you when you type in the **Text Input** box you can see how the data is first passed to the **String Format** node that then augments the data and passes it along on it's **Formatted** output. Learn more about how the [String Format](/nodes/string-manipulation/string-format) node work in the node reference documentation.
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/data/making-connections/testing-connection-2.mp4")}/>
</div>
We have covered a very important concept that is used for making data driven reactive user interfaces. But we won't get far just connecting different UI Controls together, most often we need to present data from a database, or an external API. For that we will introduce the data nodes (the green ones), and we'll start with the **Variable** in the next guide.

View File

@@ -0,0 +1,299 @@
---
title: Using Objects
hide_title: true
---
import ImportButton from '../../../src/components/importbutton'
# Using Objects
## What you will learn in this guide
Objects is a concept of the global application data model in Noodl, an object is a data structure with properties and a string identifier that can be used to access the object globally in your Noodl application. In this guide you will learn how to use the [Object](/nodes/data/object/object-node) node, the [Create New Object](/nodes/data/object/create-new-object) and the [Set Object Properties](/nodes/data/object/set-object-properties) node to create and store **Objects** in your app. Objects are _local_ meaning they will not be stored in a cloud database by default. They are very useful to store data and states that applies to a usage session of an app or a screen. They are also essential when working with **Arrays** in Noodl since only **Objects** and **Records** can be stored in a Noodl Array.
## Overview
The guide covers the following topics
- Objects in Noodl
- Creating **Objects**
- Setting Properties in **Objects**
- Dynamically creating Objects using the **Create New Object** node.
- **Objects** in **Arrays**
- Using **Objects** in Lists with the **Repeater** node
**Objects** are very similar to **Variables** in Noodl and it's recommended to go through the [Variable guide](/docs/guides/data/variables) before reading this guide.
## What's an Object in Noodl?
**Objects** in Noodl are used to hold data. **Objects** are _local_ meaning they only exist while the App is running. This is the main difference between **Objects** and [Records](/nodes/data/cloud-data/record) - **Records** are stored in a database.
Another related node in Noodl is the [Variable](/nodes/data/variable/variable-node) node. It's also local, but can only store one value, while an **Object** can store a number of values, each in a _property_. So essentially an **Object** holds a couple of data points (properties) that belong together.
Some typical cases could be information about a person (for example `First Name`, `Last Name`, `Address`, `Age`), all the information about a football match (`Home Team`, `Away Team`, `Match Date`, `Score`), etc, etc. There is no limitation on how many properties you can have.
### The `id` of an Object
All **Objects** in Noodl have an **id**. This is the unique identifyer of the **Object** and if you have two **Object** nodes in your app, and you give them the same **id** they will point to the same data. Changing properties on one of the nodes will immediately change the data any other **Object** node with the same id.
In Noodl, when a **Node** need to refer to an **Object** you use the **id** to identify it. For example, an **Array Filter** have an output called **First Item Id** which will hold the **id** of the first object in the resulting array of object after a filter action have been done. By connecting that **First Item Id** to the **Id** of an **Object** you can access its properties.
<div className="ndl-image-with-background l">
![](/docs/guides/data/objects/object-1.png)
</div>
### The properties of an Object
An Object typically have a number of properties. On the **Object** node they have to be explicitly created to be able to access them as outputs and inputs by clicking the `+` in the properties panel.
<div className="ndl-image-with-background">
![](/docs/guides/data/objects/object-properties-1.png)
</div>
Note that just because an **Object** node doesn't have a property added, it doesn't mean that the underlying **Object** don't have it. It's just that this particular node won't have the property available as input and output until you add them.
## Creating Objects
Let's get started and create some objects!
Objects are typically created in two different ways. Either you add in an [Object](/nodes/data/object/object-node) node and give it an **Id** then the object will be created (if it doesn't exists already) when used. The other way is to use the [Create New Object](/nodes/data/object/create-new-object) node and trigger its **Do** signal. That will create a unique new **Object** with a unique **Id**.
So to summarize the two main ways of creating **Objects**
- Statically create by adding an **Object** node give it an **Id**, properties and connect to the input properties.
- Dynamically create by triggering the **Do** signal in a **Create New Object** node.
Let's start with the former.
Start a new project using the "Hello World"-template. Then add in two [Buttons](/nodes/ui-controls/button) and a [Text Input](/nodes/ui-controls/text-input) node right under the root node. Remove the **Text** node and fix the layout slightly by adding som margins and padding. Change the text on the **Buttons** to "Toggle" and "Increment".
<div className="ndl-image-with-background">
![](/docs/guides/data/objects/step-1.png)
</div>
## Setting Properties
We want the "Toggle" button to toggle a [Switch](/nodes/logic/switch) from `false` to `true`, the "Increment" button to increase a [Counter](/nodes/math/counter), and the **Text Input** to generate a text. This will allow us to generate some data to put in an **Object**
So add in the **Switch** and the **Counter** and hook them up.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/objects/step-2.png)
</div>
Now let's save the values in an **Object**. So create an **Object** node, make sure the **Id** is set to `Specify explicitly`. This allows us to set an **Id** of the **Object**. The other option, `From repeater`, is used if the component with this **Object** would be a list item in a [Repeater](/nodes/ui-controls/repeater) node, which we will look at later in this guide.
<div className="ndl-image-with-background">
![](/docs/guides/data/objects/object-properties-2.png)
</div>
Give it the **Id** `Form Values`. Then add three properties: `toggle_state`, `counter_value` and `input_text`.
<div className="ndl-image-with-background">
![](/docs/guides/data/objects/object-properties-3.png)
</div>
Finally, connect the outputs to the **Object** as below.
<div className="ndl-image-with-background l">
![](/docs/guides/data/objects/step-3.png)
</div>
Now, whenever any of these outputs are changed, the new value is stored in the **Object**. This might be exactly what you want - or you might want to control much more specifically when the value is store. For example, maybe you only want to store the text from the **Text Input** when the user hits enter.
To do this more controlled set, we can use the [Set Object Properties](/nodes/data/object/set-object-properties) node. So let's add one in, make sure it's pointing to the same underlying **Object** by giving it the same **Id**, i.e. `Form Values`. You will actually see it suggested when clicking the **Id** input property.
Now you can add in the `input_text` property on the **Set Object Properties** node. You can also see that in the **Set Object Properties** node you can force what type the value should be when setting it. For example, forcing a Number, would store the number `1` rather than the string `"1"` if you type it in the **Input Text** node. However, we want it to always be strings in this case, so pick `String`.
<div className="ndl-image-with-background">
![](/docs/guides/data/objects/object-properties-4.png)
</div>
Now connect the **On Enter** signal of the **Text Input** to the **Do** input signal on the **Set Object Properties** and make sure the **Text** output is hooked up to the `input_text` property. The nodes should look like below.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/objects/step-4.png)
</div>
By hovering on the **Object** node and pinning the debug box by clicking on it, you will be able to see that the `input_text` property is only set when hitting enter.
Note the following:
- Not all properties need to be created on the **Object** or **Set Object Properties** node if you are not using them.
- You can mix and match "lazy" setting directly on the **Object** node and controlled setting using the **Set Object Properties** node depending on need.
## Using Objects as global holders of data
So let's use our **Object** in another component to show that it's available anywhere, as long as you know the **Id**. Create a new visual component and call it `Data Presentation`.
Add three [Text](/nodes/basic-elements/text) nodes to the root **Group**.
<div className="ndl-image-with-background">
![](/docs/guides/data/objects/step-5.png)
</div>
Now add om an **Object** node, give it the **id** `Form Values` and hook it up to the **Text** nodes. Don't forget to add the properties to the **Object**.
<div className="ndl-image-with-background l">
![](/docs/guides/data/objects/step-6.png)
</div>
Finally we add the new component to our main component.
<div className="ndl-image-with-background l">
![](/docs/guides/data/objects/step-7.png)
</div>
Now when changing the values you will see them updating immediately in the `Data Presentation` component.
## Dynamically creating **Objects**
We will change it up a bit and create more **Objects** - actually we will create one every time the user hits enter. So lets change our nodes. In the `Main` component, remove the **Object** and **Set Object Properties** node. Instead add in a [Create New Object](/nodes/data/object/create-new-object) node. Add our properties to the node, i.e. `toggle_state`, `counter_value` and `input_text`.
<div className="ndl-image-with-background">
![](/docs/guides/data/objects/object-properties-5.png)
</div>
Then hook up the output values to the properties as before. Also connect **onEnter** to **Do** on the **Create New Object**.
<div className="ndl-image-with-background l">
![](/docs/guides/data/objects/step-8.png)
</div>
Now whenever we hit enter we create a new **Object** with the properties set according to the **Counter**, **Switch** and **Text Input**. However the **Objects** are left somewhere in memory where we cant reach them. Each new **Object** have a new unique **Id** but we don't save that **Id** anywhere. Let's fix that! Let's put the **Objects** in an **Array**.
## Storing Objects in an Array
Add in an **Array** node. Give the **Array** the **Id** `Form Objects`. Also add a [Insert Object Into Array](/nodes/data/array/insert-into-array) node.
If you check the documentation of the **Insert Object Into Array** node, it needs the **Id** of the **Object** that should be inserted. Conveniently there is an output from our **Create New Object** that holds the **Id** of the newly created object.
To insert the **Object** into the **Array** we need to trigger the **Do** signal on **Insert Object Into Array**. Also, we should not forget to specify the **Id** of the **Array** we want to insert it into, namely the `Form Objects`.
The construct can be seen below.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/objects/step-9.png)
</div>
Now if you bring up the debug info on the **Array** by hovering on it and clicking the debug box, you will see the number of items in the **Array**. Everytime you hit enter in your App it should be growing as we are creating **Objects** and inserting into the **Array**.
## Using Objects in list with the Repeater node
To wrap up this guide, let's also show all the **Objects** we are creating. We want to put them in a list, so we are going to need a **Repeater** node and turn our `Data Presentation` component into a list item.
So start with removing the `Data Presentation` node from our main tree. Replace it with a **Repeater** node. Then connect the **Items** output of the **Array** to the **Repeater** input also called **Items**. Now we are feeding the repeater from the **Array**.
<div className="ndl-image-with-background l">
![](/docs/guides/data/objects/step-10.png)
</div>
Click on the **Repeater** node and make sure you select the `Data Presentation` node as the List Item template.
<div className="ndl-image-with-background">
![](/docs/guides/data/objects/repeater-properties-1.png)
</div>
Now if you enter som data in your app and press enter, you will actually see new items pop up in your list, but they will be empty, or contain your old data if you didnt refresh the viewer recently.
<div className="ndl-image-with-background">
![](/docs/guides/data/objects/result-1.png)
</div>
The reason is that the `Data Presentation` component is still looking for data in our old object called `Form Values` and we removed that **Object**.
Go into the `Data Presentation` component. We need to change how the **Object** there gets its **Id**.
<div className="ndl-image-with-background">
![](/docs/guides/data/objects/object-properties-6.png)
</div>
Change it to `From repeater` and you should now be able to see the correct values in the list. Clean up the layout of the list items by setting their size to content height and add some margins.
Now you know how to use **Objects** in Noodl. Import the final project by clicking the "Import" button below.
<div className="ndl-image-with-background">
![](/docs/guides/data/objects/result-2.png)
<ImportButton
zip="/docs/guides/data/objects/using-objects.zip"
name="Using Objects"
thumb="/docs/guides/data/objects/result-2.png"
/>
</div>
### Noodl Objects and Javascript
Noodl objects can be accessed from Javascript using the [Objects API](/javascript/reference/objects). A Noodl object can be accessed like a normal Javascript object but there are a few important differences. Essentially a Noodl object is like a Javascript object with a string identified, the `id` of the object which can be used to access this object anywhere in your application.
If you have the **Id** of an object you can access it like this in Javascript, e.g. in a function node:
```javascript
const obj = Noodl.Objects["Form Values"];
console.log(obj.input_text);
console.log(obj.id); // This will output "Form Values"
obj.input_text = "Something"; // This will trigger a change on the object nodes with "Form Values" id
```
Sometimes you can end up having properties in a Noodl Object that are references to other Noodl Objects, like this example:
```javascript
const post = Noodl.Objects["Post"];
post.auhtor = Noodl.Object.create({
name:"Lisa Simpson"
})
```
To access nested objects like this you can use this pattern, that is you don't have to first extract the **Id** of the nested object with something like an Expression node you can just connect it directly to the **Id** input of another Object node like this:
<div className="ndl-image-with-background xl">
![](/docs/guides/data/objects/object-nested-1.png)
</div>

View File

@@ -0,0 +1,14 @@
---
title: Working with Data in Noodl
hide_title: true
---
# Working with Data in Noodl
Noodl contains a number of nodes that help you fill your UI with data and read and process the data that the user enters. This is achieved by connecting the visual nodes to **Variables**, **Objects** and **Arrays**.
* **Variables** are singular values
* **Objects** are a collection of different values that belong together, for example data about an order or a customer
* **Arrays** are a list of **Objects** for example a list of orders or customers
These nodes are all _client side_ only, meaning they are not stored in a database in the cloud. They are also only valid during a singular session in an App. For example if the user refresh the browser this data is cleared. Storing this data in the cloud is easy however and you can learn more about that in the [Cloud Data Guides](/docs/guides/cloud-data/overview).

View File

@@ -0,0 +1,173 @@
---
title: UI Controls and data
hide_title: true
---
import useBaseUrl from '@docusaurus/useBaseUrl'
import ImportButton from '/src/components/importbutton'
# UI Controls and data
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/data/ui-controls-and-data/final-crud.mp4")}/>
<ImportButton zip="/docs/guides/data/ui-controls-and-data/final-crud-1.zip" name="CRUD Example" thumb="/docs/guides/data/ui-controls-and-data/final-crud-thumb.png" />
</div>
## What you will learn in this guide
In the guides up to this point we have often connected UI Controls, such as **Text Inputs**, to data. In this guide we will present a few patterns that is commonly used to connect data to UI controls.
## Connecting UI Controls to data
A very common pattern is that we want a UI Control to write to some sort of data node (**Variable**, **Object** etc) when it is updated. All UI Controls that allow for some sort of data input has both an input and an output for the data. For instance, the [Text Input](/nodes/ui-controls/text-input) has both an input and output called **Text**, and the [Slider](/nodes/ui-controls/slider) has an input and output called **Value**.
The output is used to write the data to some sort of data node. Most UI Controls have a **Changed** signal output that is triggered when the user has interacted with the control. This should be used as a trigger to an action that writes data. The most simple one being shown below. When the slider is updated we write the current value to a **Variable**.
<div className="ndl-image-with-background l">
![](/docs/guides/data/ui-controls-and-data/slider-set-variable.png)
</div>
This is the most basic pattern at it will make sure the **Variable** named *Volume* is updated when the slider is dragged. However, if the *Volume* variable is updated somewhere else, the slider is not updated to refelect that change. For that we can connect the **Variable** to the slider **Value** input as well. Like below:
<div className="ndl-image-with-background l">
![](/docs/guides/data/ui-controls-and-data/slider-hooked-up.png)
</div>
This can be useful to create truly reactive user interfaces. Take the simple example below, where you have both a **Text Input** and a **Slider** that are both controlling the volume. The slider updates as soon as the user interacts with it, and the text input when it's blurred (looses focus) or when you press enter.
<div className="ndl-image-with-background l">
![](/docs/guides/data/ui-controls-and-data/slider-and-text-input.png)
</div>
Hooking you controls up likes this will keep them in sync when interacting with them. Check out the result below:
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/data/ui-controls-and-data/slider-and-text-input.mp4")}/>
</div>
## Forms
Many times you want to use a form, or simply a set of UI Controls, to view and edit the data in an **Object**. You could create a component that has an input for the **object id** of the object it should edit and then use the pattern above, but instead we want to save when a button is clicked. Let's say we want to create the simple form below:
<div className="ndl-image-with-background l">
![](/docs/guides/data/ui-controls-and-data/form-1.png)
</div>
We will keep the information about a user (full name etc) in an **Object** and we can edit the object using the following simple component. Here you can see that the **Object Id** is provided as an input to this component. An **Object** node is used to "read" the data and connect it to the UI Controls, this will populate them with the correct data for the object when the **Object Id** changes. The properties are then written to the object when the **Save** button is clicked using the **Set Object Properties** node.
<div className="ndl-image-with-background l">
![](/docs/guides/data/ui-controls-and-data/form-2.png)
</div>
## Create, Read, Update & Delete
A very common pattern in web apps is CRUD (Create, Read, Update & Delete) user interfaces. This is you have a list of objects and you want to be able to create, read, update and delete these objects. Let's extend out form into a full CRUD user interface.
To begin with, we'll add a few small things to our **Form** component that we created above. First we will expose the **Mounted** property on the root **Group** node as an input. This will allow us to control the visibility of the form when using it in our CRUD interface.
<div className="ndl-image-with-background l">
![](/docs/guides/data/ui-controls-and-data/crud-1.png)
</div>
Secondly, we will add a cancel button and send both the save button and cancel button **Clicked** signals as outputs from this component.
<div className="ndl-image-with-background l">
![](/docs/guides/data/ui-controls-and-data/crud-2.png)
</div>
Then we end up with this. This is now our **Form** visual component. It will be used both for editing and creating new objects.
<div className="ndl-image-with-background l">
![](/docs/guides/data/ui-controls-and-data/crud-3.png)
</div>
Now lets start on a new component, let's call this one **Members List**. Imaging the we want to manage a list of members with their name, if they want our marketing email and their general awesomeness. First we create the members list. We won't cover things like repeaters in detail in this guide, check out the [list](/docs/guides/data/list-basics) guide.
<div className="ndl-image-with-background l">
![](/docs/guides/data/ui-controls-and-data/crud-4.png)
</div>
We have a [Repeater](/nodes/ui-controls/repeater) node that is connected to an [Array](/nodes/data/array/array-node) that we have given the **Id** "Members". This array will be empty to start with. We also have a button that we will later use to show the create form. We also need a **Member Item** that the repeater will use as template.
In the **Member Item** component, we simply show the full name of the object, and we include a delete button.
<div className="ndl-image-with-background l">
![](/docs/guides/data/ui-controls-and-data/crud-5.png)
</div>
Now, let's add the form component to our **Members List** component to support creating new members. This is what that looks like:
<div className="ndl-image-with-background l">
![](/docs/guides/data/ui-controls-and-data/crud-6.png)
</div>
Let's review the different parts here:
* We created a [States](/nodes/utilities/logic/states) node and gave it three different states, **Create**, **Update** and **List**. These states will be used to toggle the visibility of our different parts of the user interface. We also made sure it starts in the **List** state.
* When the **Add member** button is clicked, a **Create New Object** node is triggered. In this node we set the default properties of the new member object. That means that the newly created object will start with these properties.
* When the **Create New Object** is done the states node is switched to **Create** state and the **Id** of the newly created object is passed to the **Object Id** input of the **Form** component that we created before.
* The **At List** and **At Create** outputs from the states node is used to control the visibility of the member list and the create form. So that changing the state of the **States** node will show the correct component.
* Finally, when the **Save** signal is sent from the **Form** component (the user has clicked the save button) the object is inserted into the **Members** array.
Phew, now we have the create part in place. Let's add the read and update part as well. For that we will just first add a small thing to the **Member Item** component. We will send an event with the [Send Event](/nodes/events/send-event) node when the text label is clicked, and we will pass along the object **Id** of the clicked object.
<div className="ndl-image-with-background l">
![](/docs/guides/data/ui-controls-and-data/crud-7.png)
</div>
We simply called the event **Edit**, now we can receive the event in the **Members List** using the [Receive Event](/nodes/events/receive-event) node component and use it to toggle the **States** node to **Edit** state. As you can see we also added another instance of the **Form** component at the bottom of the **Members List** component and we hooked it up to the **States** node to control it's visibility.
<div className="ndl-image-with-background l">
![](/docs/guides/data/ui-controls-and-data/crud-8.png)
</div>
Now that we have both create, read and update in place this is what we end up with in the **Members List** component.
<div className="ndl-image-with-background l">
![](/docs/guides/data/ui-controls-and-data/crud-9.png)
</div>
Now we just have one final thing left, and that is to hook up the delete button in the **Member Item** component. That is pretty easy to do, we simple connect the **Click** signal to an **Remove Object From Array** making sure the we specify the array id **Members** that we used in the **Members List** component (so we are changing the right array) and then provide the object **Id** to remove as well.
<div className="ndl-image-with-background l">
![](/docs/guides/data/ui-controls-and-data/crud-10.png)
</div>
And there we go, now we have create the a fully functional CRUD user interface:
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/data/ui-controls-and-data/final-crud.mp4")}/>
<ImportButton zip="/docs/guides/data/ui-controls-and-data/final-crud-1.zip" name="CRUD Example" thumb="/docs/guides/data/ui-controls-and-data/final-crud-thumb.png" />
</div>

View File

@@ -0,0 +1,308 @@
---
title: Using Variables
hide_title: true
---
import ImportButton from '../../../src/components/importbutton'
# Using Variables
## What you will learn in this guide
In this guide you will learn how to use the [Variable](/nodes/data/variable/variable-node) node and the [Set Variable](/nodes/data/variable/set-variable) node to store data in your application for later use. Variables are _local_ meaning they will not be stored in a persistent database. They are very useful to store data and states that applies to a usage session of an app or a screen.
## Overview
The guide covers the following topics
- Setting variables
- Reading variables
- Accessing variables globally
- Using variables with conditions
## When to use Variables?
When developing an app, you often run into the situation that you need to hold on to a value - a piece of data - and use it at a different place in your app.
For example, you may have a [TextInput](/nodes/ui-controls/text-input) where the user can input their name. You want the name first letter of the name to be always capitalized, no matter what the user inputs it. This name should be used through out the App. Hence you would like to store it so you can retrieve it on various places in the app.
You cannot really make a connection directly from the **TextInput** node to all the places where the name will be used. First of all, there might be too many of those connections. Second of all, the nodes that need the name are maybe not in the same component so you can't really reach them. And lastly, you don't want the intermittent name, while the user is entering it, you only want it when the user is done, and then you want it with a capital first letter and not exactly how the user wrote it. This is use one case of many, where a Variable comes in handy.
## Storing and reading variables
Let's start by opening a new project. You can base it on the Hello World template. Then add a **TextInput** node. Change its' label to "Name". You may want to add a little bit of padding to the parent container as well, to make sure the **TextInput** get some air around itself.
<div className="ndl-image-with-background">
![](/docs/guides/data/variables/nodes-1.png)
</div>
<div className="ndl-image-with-background">
![](/docs/guides/data/variables/screen-1.png)
</div>
Now we are going to do two things. 1. We want to store the value of the TextField in a **Variable** node when the user hits _Enter_. 2. We also want to capitalize the first letter of the name, before we store it.
Let's start with the capitalization. Add a [Function](/nodes/javascript/function) node. Edit the script and paste the following:
```javascript
if (Inputs.name !== undefined) {
Outputs.capitalizedName =
Inputs.name.substr(0, 1).toUpperCase() + Inputs.name.substr(1)
}
```
<div className="ndl-image-with-background xl">
![](/docs/guides/data/variables/function-1.png)
</div>
It's not important right now that you understand exactly how the Javascript works, but it will capitalize the first letter of what you feed into the **Function** node and set it on the output named `capitalizedName`.
Connect the **Text** output from the **TextInput** node to the **name** input on the newly created **Function** node.
<div className="ndl-image-with-background l">
![](/docs/guides/data/variables/nodes-2.png)
</div>
### Setting a Variable
Ok so let's store the capitalized name in a **Variable**. To do this, add a **Set Variable** node.
<div className="ndl-image-with-background l">
![](/docs/guides/data/variables/nodes-3.png)
</div>
### The Variable Name
Each variable need a **Name**. Any **Variable** or **Set Variable** node anywhere in your App that has the same **Name** will refer to the same actual data. The **Name** is case sensitive, and we are going to pick the name `userName`.
We will also set the type of the **Variable** to a **String**. This is important, for example, if the user chooses a username which are only numbers. With the **String** type set, we know even in that case, what's stored in this **Variable** is a **String**.
<div className="ndl-image-with-background">
![](/docs/guides/data/variables/panel-1.png)
</div>
Then connect the `capitalizedName` output from the **Function** node to the **Value** input of the **Set Variable** node.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/variables/nodes-4.png)
</div>
<div className="ndl-image-with-background xl">
![](/docs/guides/data/variables/nodes-5.png)
</div>
We have one more thing we need to do to actually store the value in the **Variable**. We need to trigger the **Do** signal in the **Set Variable** node. This makes it possible for us to control exactly what we save. Again, as the user is typing in the **TextInput** the `capitalizedName` output will keep updating (try it!) and we only want to save once the user press _Enter_.
So, connect the **onEnter** output on the **TextField** to the **Do** input signal on the **Set Variable** and we are done with storing data in a **Variable**.
<div className="ndl-image-with-background l">
![](/docs/guides/data/variables/nodes-6.png)
</div>
### Reading a Variable
Ok, now we want to use the value we just stored. We want to include the name in the `Hello World` greeting. So lets first add a [String Format](/nodes/string-manipulation/string-format/) node. Make sure the text in the node is "Hello World {name}".
<div className="ndl-image-with-background xl">
![](/docs/guides/data/variables/nodes-7.png)
</div>
<div className="ndl-image-with-background">
![](/docs/guides/data/variables/panel-2.png)
</div>
The **String Format** node now should have an input called **name**.
Create a **Variable** node. Make sure its' name is `userName` (it should pop up as a suggestion when you click the **Name** input in the panel), just as before. This **Variable** node will now refer to the same **Variable** as we were storing before.
Connect the **Value** output of the **Variable** to the **name** input of the **String Format** node. Finally, connect the **Formatted** output from the **String Format** node to the **Text** input on the **Text** node.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/variables/nodes-8.png)
</div>
Now test your app. Start writing a name in the **Text Input**, press _Enter_ and you should see the "Hello World"-text changing.
<div className="ndl-image-with-background">
![](/docs/guides/data/variables/screen-2.png)
</div>
## Accessing Variables Globally
Now let's change the structure a little. We will create a **Popup** and present the "Hello World" message in it only after the user have pressed _Enter_.
:::note
If you want to learn more about popups, take a look at the [Popups](/docs/guides/navigation/popups) guide.
:::
Start by creating a new visual component, call it "Hello Popup".
<div className="ndl-image-with-background">
![](/docs/guides/data/variables/create-popup-component-1.png)
</div>
This will become our popup. In the new component. Change the size so it takes up 80% of the width and 50% of the height. Make it's layout "Absolute". Align it in the center. Also change the color to a nice color, perhaps blue.
<div className="ndl-image-with-background">
![](/docs/guides/data/variables/popup-panel-1.png)
</div>
<div className="ndl-image-with-background">
![](/docs/guides/data/variables/popup-panel-2.png)
</div>
Now, we want to move the **Text** node and the nodes feeding it with the text from the App component to the popup. Go back to the "App" component and selecte all nodes. Then copy them (Ctrl+C/Cmd+C). Go back to the "Hello Popup" component and paste the nodes.
Remove all nodes except the **Text** node, the **String Format** node and the **Variable** node. Then drag in the **Text** node so it becomes the only child of the **Group** node.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/variables/nodes-9.png)
</div>
Then remove the same nodes from the "App" component.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/variables/nodes-10.png)
</div>
We have now moved the "Hello World" text into a Popup. So let's open the Popup using the [Show Popup](/nodes/popups/show-popup) node. Create the node and connect the **Done** signal from the **Set Variable** node to the **Show** signal on the **Show Popup** node.
<div className="ndl-image-with-background xl">
![](/docs/guides/data/variables/nodes-11.png)
</div>
Also make sure the component that's being shown as a popup is the "Hello Popup".
<div className="ndl-image-with-background l">
![](/docs/guides/data/variables/popup-panel-3.png)
</div>
To back up for a second; what we have done now is to set the Variable `userName` when the user hits _Enter_. When the Variable have been set (the **Done** signal) we open a Popup with a new component. The component access the same Variable and shows the "Hello Message".
Before trying it, we should probably make it possible to close the popup.
In the "Hello Popup" component. Add a [Close Popup](/nodes/popups/show-popup) node and connect the **Click** signal from the parent group to the **Close** signal.
<div className="ndl-image-with-background l">
![](/docs/guides/data/variables/nodes-12.png)
</div>
As you can see from this example, Variables are available in any component in your App.
<div className="ndl-image-with-background l">
![](/docs/guides/data/variables/screen-3.png)
</div>
## Using Variables with Conditions
A very useful way to use Variables is when you want your App to show different things depending on certain conditions. For example, if the user enters a name that's too short, you want the text to ask them to pick another name. Otherwise you want to show the Hello message.
The general pattern is the following: Have a Variable that holds the final value to be used by a component (in this case a text to be shown by a **Text** node). Then use a [Condition](/nodes/utilities/logic/condition) to trigger two different **Set Variable** nodes, depending on the condition. The pattern is outlined below.
<div className="ndl-image-with-background l">
![](/docs/guides/data/variables/pattern-1.png)
</div>
So let's try it on our example. We want to check for the case when the user name is less than 6 characters and ask the user to enter a new and longer name.
So using the pattern above we introduce a new **Variable** in the "Hello Popup". Let's call it `popupText`. We connect its value to the text node and remove the old connection.
<div className="ndl-image-with-background l">
![](/docs/guides/data/variables/nodes-13.png)
</div>
Now we need to check the length of the provided name. This is easy using a combination of a [String](/nodes/data/string) node, an [Expression](/nodes/math/expression) node and a [Condition](/nodes/utilities/logic/condition) node.
Connect the **value** output from the **Variable** `userName` to the a **String** node. There is an output from the **String** called **Length** which holds the length (i.e. number of characters) of the string.
Then create an **Expression** node. We want to check that the length of the name is 6 characters or more. So edit the expression and write
```
length >= 6
```
<div className="ndl-image-with-background">
![](/docs/guides/data/variables/expression-1.png)
</div>
The expression node should now have an input called **length**. Connect the **Length** output from the **String** node to that input.
Finally connect the **Result** output to the **Condition** input of a **Condition** node.
<div className="ndl-image-with-background l">
![](/docs/guides/data/variables/nodes-14.png)
</div>
The **Condition** node has two signal outputs, **on True** and **on False**. We will use these signals to either set the previous "Hello World" string, or the warning string about the too short name. Let's set it up using a **String** node with a predefined error string and the **Set Variable** nodes we used before.
<div className="ndl-image-with-background l">
![](/docs/guides/data/variables/nodes-15.png)
</div>
Try it out with a few different names and see that it works. You may have to tweak the layout of the text to make it look nicer.
You can import the whole project in Noodl by clicking import below.
<div className="ndl-image-with-background l">
![](/docs/guides/data/variables/total-1.png)
<ImportButton
zip="/docs/guides/data/variables/using-variables.zip"
name="Using Variables"
thumb="/docs/guides/data/variables/screen-3.png"
/>
</div>

View File

@@ -0,0 +1,49 @@
---
title: Deploy an App on the Sandbox domain
hide_title: true
---
# Deploy an App to the Sandbox domain, `*.sandbox.noodl.app`
## What you will learn in this guide
This guide will teach you how to deploy a Noodl App to the Noodl Sandbox, i.e the `sandbox.noodl.app` domain.
You can use this to share your Noodl App with other people.
## Overview
There are multiple ways for you to deploy Noodl Apps so other people can use them. The easiest one is to use the Noodl Sandbox deployment. The your app will get a public URL that ends with `.sandbox.noodl.app`.
The video below walks through all deployment options in Noodl, including Sandbox deploys:
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/M97-89RiboE" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe>
## Doing a Sandbox Deployment
Open the project you want to deploy. Then click the **Deploy** button on the top right.
<div className="ndl-image-with-background s">
![](/docs/guides/deploy/deploying-an-app-on-sandbox/deploy-button.png)
</div>
In the popup that opens, you can select a subdomain name. Your URL will become `<subdomain>.noodl.app`. Note that some subdomains may be taken by other users, so be ready to come up with a unique name if that's the case.
<div className="ndl-image-with-background l">
![](/docs/guides/deploy/deploying-an-app-on-sandbox/noodl-deploy-to-noodl-app-domain.png)
</div>
## Managing your Sandbox Deployments
Once you have deployed your app they will be available in the list of Sandbox Deployments.
<div className="ndl-image-with-background l">
![](/docs/guides/deploy/deploying-an-app-on-sandbox/manage-deploys.png)
</div>
You can select any of the sandbox deploys and change which backend to use or to **Delete** the deploys.

View File

@@ -0,0 +1,95 @@
---
title: Deploying to Custom Domain
hide_title: true
---
# Deploying to your custom domain
## What you will learn in this guide
This guide will take you through the steps needed for deploying a Noodl app to your own custom domain. Note that the app is still hosted by Noodl, but you let your custom domain point to the app. If you want to host the backend and frontend yourself check out the [Using a self hosted backend](/docs/guides/deploy/using-an-external-backend) and [Hosting Frontend](/docs/guides/deploy/hosting-frontend).
:::note
To be able to follow this guide you must have custom domain feature enabled. This is not available in the free Noodl plan. But you can request a trial [here](https://noodl.net/plans).
:::
## Overview
The guide walks you through the following topics
- Acquiring a domain
- Deploying to your acquired custom domain
- Managing your custom domain deployments
You can also check out the video below that walks through all deployment options in Noodl, including deploying to a Custom Domain:
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/M97-89RiboE" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe>
## Acquiring your domain
The first thing you need to do is to acquire the domain, i.e. purchase it from a domain provider if you don't have it already. There are many different places where you can acquire a domain and they all work a little differently. The important thing is that you need to be able to configure the DNS records and especially set a [CNAME](https://en.wikipedia.org/wiki/CNAME_record) record. You will need this later.
## Deploying to you domain
Once you have acquired the domain, click the **Deploy application** icon in the top right corner.
<div className="ndl-image-with-background s">
![](/docs/guides/deploy/deploying-to-custom-domain/deploy-button.png)
</div>
Then select the **Custom Domains** tab.
<div className="ndl-image-with-background l">
![](/docs/guides/deploy/deploying-to-custom-domain/custom-domains.png)
</div>
Click **Manage Domains**. This allows you to add a new custom domain.
<div className="ndl-image-with-background xl">
![](/docs/guides/deploy/deploying-to-custom-domain/custom-domains-2.png)
</div>
Enter the name of the domain you want to deploy to, including the subdomain. Note that Noodl does not support naked domains (e.g. `mydomain.com`) so you will have to add a subdomain, for example `www.`.
Click **Add Domain**. Noodl will now try to connect to the domain which will always fail the first time. You need to set up a CNAME record in the DNS on your domain providers side that matches Noodl.
Copy the value in the **Value** box (`proxy-ssl.noodl.cloud`). You will need to provide when you create the CNAME record on the domain side.
<div className="ndl-image-with-background l">
![](/docs/guides/deploy/deploying-to-custom-domain/custom-domains-3.png)
</div>
Create the CNAME record in the DNS at your domain provider. Once that's done, click "Verify Connection" to see if Noodl can coonect.
?> Note that it could take some time - up to several hours - for your domain provider to propertly update the DNS records so if Noodl cannot verify the domain, try again in a few minutes or hours.
Once the domain has been verified you will see the green **Connected** text.
<div className="ndl-image-with-background l">
![](/docs/guides/deploy/deploying-to-custom-domain/custom-domains-4.png)
</div>
Now your domain is available to deploy to. So close the **Manage Domains** popup.
You can now choose your newly added domain as a target. Make sure to also pick the backend you want to use in the deploy.
<div className="ndl-image-with-background l">
![](/docs/guides/deploy/deploying-to-custom-domain/custom-domains-5.png)
</div>
Press **Create Deploy** and your new deployment is ready.
## Managing your custom deployments
Once you have added your custom domain and deployed to it you can updated it whenever you like, delete it or update which backend it uses.

View File

@@ -0,0 +1,30 @@
---
title: Deploying to iOS and Android
hide_title: true
---
# Deploying to iOS and Android
:::note
**Self Hosting**<br/>Note: To be able to follow this guide you must have "Self Host" feature enabled. This is not available in the free Noodl plan. But you can request a trial [here](https://noodl.net/plans).
:::
## What you will learn in this guide
This video will take you through the process of deploying a Noodl app as a native app using [Capacitor](https://capacitorjs.com/).
## Overview
The video walks you through the following topics:
- Introduction to the project
- Prerequisites
- Installing Capacitor and initializing a Capacitor project
- Deploying your Noodl project
- Building a native Android app
- Build an iOS app
- Adding custom app icon
## Video
<iframe width="560" height="315" src="https://www.youtube.com/embed/W44hTg8vL_g" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

View File

@@ -0,0 +1,54 @@
---
title: Embedding / iframe / Micro Frontend
hide_title: true
---
# Embedding / iframe / Micro Frontend
Noodl is a powerful tool for creating iframe and micro frontends.
Micro Frontend, iframe, and embedding are all techniques used in web development for integrating different components or applications into a single web page. However, they have distinct approaches and use cases.
## Embedding
Embedding is a more general term that refers to the process of including one piece of content within another, usually referring to the iframe technique.
### Embedding inside a Noodl app
If you are looking to embed other websites (example youtube) into Noodl, have a look at the [Custom HTML module](https://docs.noodl.net/2.9/library/modules/custom-html/).
## iframe
An iframe (inline frame) is an HTML element that allows you to embed another HTML document within a parent HTML document. By using an iframe, you can display content from another website or application within your web page without affecting the main page's layout or styling. This is useful for embedding third-party content like maps, videos, or widgets. However, iframes have some limitations, such as security risks, lack of responsiveness, and difficulty in communication between parents and iframe content.
### Add an iframe to a website
To add an iframe to your HTML document, you can use the `<iframe>` tag with the `src` attribute specifying the URL of the content you want to embed. Here's an example:
```html
<iframe
width="560"
height="315"
src="https://example.sandbox.noodl.app/"
rameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
```
In this example, the `<iframe>` tag has several attributes:
- `width` and `height`: Set the dimensions of the iframe.
- `src`: The URL of the Noodl sandbox to be embedded.
- `frameborder`: Set to "0" to remove the border around the iframe.
- `allow`: Specifies a list of features that are allowed within the iframe, such as accelerometer, autoplay, clipboard-write, encrypted-media, gyroscope, and picture-in-picture.
- `allowfullscreen`: Allows the iframe to go full-screen mode when the full-screen API is used.
This example demonstrates how to add an iframe to your HTML document to embed a Noodl sandbox. You can customize the attributes as needed to suit your specific use case or other types of content to be embedded using iframes.
## Micro Frontend
Micro Frontends is an architectural pattern that involves breaking down a frontend application into smaller, more manageable, and independent parts, called micro frontends. Each micro frontend is a self-contained unit of the frontend code that is responsible for a specific set of features or functionality.
The micro frontend approach allows for greater flexibility and scalability in frontend development, as each micro frontend can be developed and deployed independently, and can be composed to create the overall frontend application. This approach also allows for different teams to work on different parts of the front-end application, using different technologies.
If you are interested in hearing more, [contact us](https://www.noodl.net/community).

View File

@@ -0,0 +1,26 @@
---
title: How to add a Favicon?
hide_title: true
---
# How to add a Favicon?
A favicon is a small image displayed next to the page title in the browser tab.
:::tip
A favicon is a small image, so it should be a simple image with high contrast.
:::
Firstly add your favicon to the project folder,
for example `favicon.ico` in this example.
This can be done by either dragging the favicon into the project or opening the folder where the project is saved.
Next, add this to the **Head Code** in project settings:
```html
<link rel="icon" type="image/x-icon" href="/favicon.ico">
```
Your browser tab should now display your favicon image to the left of the page title.

View File

@@ -0,0 +1,37 @@
---
title: Self Hosting your Noodl frontend
hide_title: true
---
# Self hosting your Noodl frontend
## What you will learn in this guide
In this guide you will learn how to **Self Host** by deploying your Noodl App frontend to a local folder and then host it on either Google Cloud Platform or AWS. This is needed if you want to host your Noodl App frontend in your own cloud infrastructure and with your own domain name.
:::note
**Self Hosting**<br/>Note: To be able to follow this guide you must have "Self Host" feature enabled. This is not available in the free Noodl plan. But you can request a trial [here](https://noodl.net/plans).
:::
## Overview
By default, Noodl will host you App (both backend and frontend), by deploying your app to a **sandbox.noodl.app** domain for free or using your own domain. See [this](/docs/guides/deploy/deploying-an-app-on-sandbox) guide for more information.
However, for various reasons, you may want to host your frontend on your own infrastructure. For this you will first deploy it locally and then upload it to your hosting provider of choice.
## Deploying to Local Folder
By using the **Self Hosting** section in the Noodl deployment popup will save a version of your frontend to a folder on your local machine, this folder will contain everything needed and you can simply upload the content to your hosting service.
<div className="ndl-image-with-background l">
![](/docs/guides/deploy/hosting-frontend/noodl-deploy-to-folder.png)
</div>
Make sure you pick the backend you want to use for the deploy.
## Hosting your Frontend at GCP or AWS
- **Google Cloud Platform** If you would like use GCP to host the frontend you can follow this [guide](https://cloud.google.com/storage/docs/hosting-static-website). There you will create a bucket, a load balancer and a CDN.
- **Amazon Web Services** If you prefer to use AWS you can watch this [video](https://www.youtube.com/watch?v=BpFKnPae1oY&ab_channel=AmazonWebServices) it will explain how to create an S3 bucket and how to route traffic using the AWS DNS service, Route 53.

View File

@@ -0,0 +1,10 @@
---
title: Deploying and hosting Noodl apps
hide_title: true
---
# Deploying and hosting Noodl apps
Noodl have it's own hosting infrastructure which makes it very easy to deploy Noodl app. You can also use your own custom domain to point to the app if you have one.
If you for some reason want to host the frontend and/or the backend on you own infrastructure that's also possible.
### [Start learning about hosting and deployment of Noodl apps](/docs/guides/deploy/deploying-an-app-on-sandbox)

View File

@@ -0,0 +1,47 @@
---
title: Project Structure
hide_title: true
---
# Project Structure
You can find the project folders at this path:
Windows Path:
```
%AppData%\Roaming\Noodl\projects
```
MacOS Path:
```
~/Library/Application Support/Noodl/projects
```
You can also open the project via this Button inside Noodl, in the project settings.
<div className="ndl-image-with-background l">
![](/docs/guides/deploy/open_project_folder.png)
</div>
## What is deployed?
All files in the project folder is deployed to the frontend with a few exceptions.
List of a few files that are ignored:
```
.gitattributes
.gitignore
project.json
```
List of a few folders that are ignored:
```
.git/
.noodl/
```
_These lists might not be complete._
When deploying the app Noodl will also add a few new files for example React library.

94
docs/guides/deploy/pwa.md Normal file
View File

@@ -0,0 +1,94 @@
# Progressive Web App
A Progressive Web App (PWA) is a type of web application that combines the best features of both web and native mobile applications. It delivers a fast, reliable, and engaging user experience across different devices and platforms, without the need to install an app from an app store. PWAs use modern web technologies to provide app-like functionality, such as offline access, push notifications, and access to device hardware.
As of today (Noodl 2.8.3) there are no built-in features to make it **easier** to work with Progressive Web Apps in Noodl.
If you are interested in hearing more, [contact us](https://www.noodl.net/community).
## Key characteristics
Here are some of the key characteristics of Progressive Web Apps.
### Connectivity independence
One of the key features of PWAs is their ability to work offline or with poor network conditions. This is achieved through caching strategies implemented using service workers, which store assets and data locally on the user's device.
Service workers are one of the ways to achieve this. JavaScript files that run in the background, separate from the main browser thread. They enable core PWA features like offline functionality, caching, background synchronization, and push notifications. Service workers act as a proxy between the web app and the network, allowing developers to control how network requests are handled and implement efficient caching strategies.
### Fast and reliable performance
They can work offline or with poor network conditions, using cached data and assets to provide a functional user experience.
PWAs use caching and background data synchronization to ensure a smooth and fast user experience.
Web workers are a powerful tool for enhancing web application performance. They execute JavaScript code in the background, separate from the main browser thread, allowing computationally intensive tasks to be offloaded without affecting the responsiveness of the user interface. Web workers communicate with the main thread using a messaging system, ensuring seamless integration with the rest of the application. By making it possible to run complex operations in parallel, web workers significantly improve the overall user experience, particularly in CPU-demanding applications.
There is a module built to work with Web Workers in Noodl, it is quite technical, and it can be found here [Web Worker Module](https://github.com/noodlapp/modules/tree/main/modules/web-worker).
### Installable
With Progressive Web Apps you can add your Noodl app to a device's home screen, similar to how native apps appear. Installing a PWA creates a shortcut and allows you to access the web app directly without needing to open a browser and navigate to the app's URL.
The installation process for PWAs is relatively simple and does not require downloading from an app store. Once a PWA meets certain criteria (e.g., having a web app manifest and being served over HTTPS), browsers may prompt users to install the PWA or provide an option to do so manually.
Once installed, the PWA can be launched from the home screen, app drawer, or any other location where native apps can be accessed. The PWA will open in a standalone window, without the typical browser interface elements, making it feel more like a native app.
## How to create a PWA?
### Creating the Web App Manifest
The web app manifest is a JSON file that provides metadata about the PWA, such as its name, icons, display settings, and preferred orientation. This information allows the PWA to be installed on a device's home screen with a custom icon and launch experience, making it feel more like a native app.
1. Create a manifest.json file and include it in your Noodl apps [project directory](/docs/guides/deploy/project-structure/).
2. Open the manifest.json file and add the basic properties required for your web app.
Here's a simple example:
```json
{
"name": "My PWA",
"short_name": "PWA",
"description": "A sample Progressive Web App",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000"
}
```
Here's a brief explanation of each property:
- `name`: The full name of your Progressive Web App.
- `short_name`: A shorter name for your PWA, used when there's limited space (e.g., on the home screen).
- `description`: A brief description of your PWA.
- `start_url`: The URL that the PWA should open when launched.
- `display`: The preferred display mode for the PWA. "standalone" is the most app-like experience, without browser interface elements.
- `background_color`: The background color of the
3. Make sure to reference it in your Head Code which can be found under the Project Settings, using a link tag:
```html
<link rel="manifest" href="/manifest.json" />
```
4. Deploy your application and you are now using the basic functionalities of Progressive Web Apps.
### Verify Web App Manifest on a Phone
Now when you have installed the manifest file, there are different ways to test it.
This is one of the ways it can be tested on a phone by following these steps:
1. Open the web app on your phone's browser using by going to your Noodl app URL.
2. Add the web app to your phone's home screen:
- **Android (Chrome)**: Tap the vertical ellipsis (⋮) in the top-right corner of the browser, then select "Add to Home screen" or "Install App" from the menu. Confirm the action by tapping "Add" or "Install" when prompted.
- **iOS (Safari)**: Tap the "Share" icon at the bottom of the browser (a square with an arrow pointing upwards), then scroll down and select "Add to Home Screen." Confirm by tapping "Add" in the top-right corner.
3. Close your browser and find the app icon on your phone's home screen.
4. Tap the app icon to launch the PWA. It should open in a standalone window without the browser interface elements, providing an app-like experience.
Please note that this method only checks if your web app can be added to the home screen and launched like a PWA. It doesn't evaluate the full PWA functionality or features such as offline support, push notifications or performance optimizations.
To thoroughly test your PWA, you should look at using Lighthouse in Chrome Developer Tools.

View File

@@ -0,0 +1,87 @@
---
title: Setting up a backend on Amazon Web Services
hide_title: true
---
# Setting up a backend on Amazon Web Services
This guide will cover how to start up a Noodl backend on AWS using the Noodl backend docker image. You will need an AWS account setup. The service you will be using is called **App Runner**. Start by going to the console for that service.
<div className="ndl-image-with-background xl">
![](/docs/guides/deploy/using-an-external-backend/aws-1.png)
</div>
There you can create a new service.
<div className="ndl-image-with-background m">
![](/docs/guides/deploy/using-an-external-backend/aws-2.png)
</div>
When setting up the new service you need to specify that the image is to be fetched from the container registry.
<div className="ndl-image-with-background xl">
![](/docs/guides/deploy/using-an-external-backend/aws-3.png)
</div>
The image URL to use is:
```bash
public.ecr.aws/noodl/noodl-self-hosted-cloud-services:latest
```
You can choose if you want to manually control when you redeploy and instance. This is needed if the Noodl backend image is updated and you want to apply the updates to your service. You can also choose to automatically track changes. When you are done click **Next**.
On the next screen you provide a **name** for your service and you can change settings for service. For the most part you can keep the standard settings but a few needs to be changed.
* **Port** The port of the application needs to be set to **3000**
You also need to provide a few environment variables to the instance. You do this using the **Add environment variable** button.
<div className="ndl-image-with-background xl">
![](/docs/guides/deploy/using-an-external-backend/aws-4.png)
</div>
The following variables are needed:
* **APP_ID** You can choose this yourself, you need to provide it in the Noodl editor when connecting to your backend.
* **MASTER_KEY** This you be a password that you need to keep safe. With this password you get full access to your backend, this is also needed to connect to your backend from Noodl.
* **DATABASE_URI** This is the database url that you got when setting up the MongoDB database in the previous step. This can also be a Postgres url.
* **PUBLIC_SERVER_URL** (Optional) This is needed if you want to support file uploads and downloads, in that case you might need to go back here and update this environment variable after you have received the URL in the next step. This variable should be the public url where your clour services can be reached, starting with `https://`.
With that in place you can move on to reviewing your settings and deploying your service. It might take a few minutes to completely setup you new service.
Once the service is up and running the last step is to find the URL of the service. You can find it by navigating to the service details page. It will look something like this:
<div className="ndl-image-with-background l">
![](/docs/guides/deploy/using-an-external-backend/aws-5.png)
</div>
With that URL, master key and app id in hand, [go back](/docs/guides/deploy/using-an-external-backend#connect-your-application-to-the-self-hosted-backend) to the self hosting guide.
# File storage
If you want to support file upload and download, you need to specify the **PUBLIC_SERVER_URL** environment variable as noted above. By default the files will be stored in the database of your application, but if you instead want to use an S3 bucket for storage you need to create a bucket, and policy, you can find instructions on that [here](http://docs.parseplatform.org/parse-server/guide/#configuring-s3adapter). Then specify these additional environment variables:
* **S3_ACCESS_KEY** The AWS access key for a user that has the required permissions. Required.
* **S3_SECRET_KEY** The AWS secret key for the user. Required.
* **S3_BUCKET** The name of your S3 bucket. Needs to be globally unique in all of S3. Required.
* **S3_REGION** (Optional) The AWS region to connect to. Default: us-east-1
* **S3_BUCKET_PREFIX** (Optional)Create all the files with the specified prefix added to the filename. Can be used to put all the files for an app in a folder with folder/.
* **S3_DIRECT_ACCESS** (Optional)Whether reads are going directly to S3 or proxied through your Parse Server. If set to true, files will be made publicly accessible, and reads will not be proxied. Default: false

View File

@@ -0,0 +1,84 @@
---
title: Setting up a backend on Google Cloud Platform
hide_title: true
---
# Setting up a backend on Google Cloud Platform
This guide will cover how to start up a Noodl backend on GCP using the Noodl backend docker image. You will need a GCP account setup. The service you will be using is called **Cloud Run**. Start by going to the console for that service.
<div className="ndl-image-with-background m">
![](/docs/guides/deploy/using-an-external-backend/gcp-1.png)
</div>
There you can create a new service. Find the button **Create Service** at the top.
When setting up the new service you need provide the url to the Noodl backend docker image.
<div className="ndl-image-with-background xl">
![](/docs/guides/deploy/using-an-external-backend/gcp-2.png)
</div>
The image URL to use is:
```bash
gcr.io/noodlapp/noodl-self-hosted-cloud-services:latest
```
Another important setting is making sure that unauthenticated requests can be handled by your new service.
<div className="ndl-image-with-background xl">
![](/docs/guides/deploy/using-an-external-backend/gcp-3.png)
</div>
For the most part you can keep the standard settings but a few needs to be changed. These can be found by expanding the **Container, Connections, Security** section.
* **Continer port** The port of the application needs to be set to **3000**
You also need to provide a few environment variables to the instance. You do this using the **+ Add Variable** button.
<div className="ndl-image-with-background xl">
![](/docs/guides/deploy/using-an-external-backend/gcp-4.png)
</div>
The following variables are needed:
* **APP_ID** You can choose this yourself, you need to provide it in the Noodl editor when connecting to your backend.
* **MASTER_KEY** This you be a password that you need to keep safe. With this password you get full access to your backend, this is also needed to connect to your backend from Noodl.
* **DATABASE_URI** This is the database uri that you got when setting up the MongoDB database in the previous step. This can also be a Postgres uri.
* **PUBLIC_SERVER_URL** (Optional) This is needed if you want to support file uploads and downloads, in that case you might need to go back here and update this environment variable after you have received the URL in the next step. This variable should be the public url where your clour services can be reached, starting with `https://`.
With that in place you can create your new service, hit the **Create** button.
Once the service is up and running the last step is to find the URL of the service. You can find it by navigating to the service details page.
<div className="ndl-image-with-background l">
![](/docs/guides/deploy/using-an-external-backend/gcp-5.png)
</div>
With that URL, master key and app id in hand, [go back](/docs/guides/deploy/using-an-external-backend#connect-your-application-to-the-self-hosted-backend) to the self hosting guide.
# File storage
If you want to support file upload and download, you need to specify the **PUBLIC_SERVER_URL** environment variable as noted above. By default the files will be stored in the database of your application, but if you instead want to use a GCS bucket for storage you can provide the following environment variables.
* **GCP_PROJECT_ID** The project ID from the Google Developers Console. Required.
* **GCS_BUCKET** The name of your GCS bucket. Required.
* **GCP_CLIENT_EMAIL** The client email of the service account with permissions to the bucket.
* **GCP_PRIVATE_KEY** The private key associated with the client email for the servive account with permissions to the bucket.
* **GCS_BUCKET_PREFIX** (Optional) Create all the files with the specified prefix added to the filename. Can be used to put all the files for an app in a folder with folder/.
* **GCS_DIRECT_ACCESS** (Optional) Whether reads are going directly to GCS or proxied through your Parse Server. Default: false

View File

@@ -0,0 +1,154 @@
---
title: Using a self hosted backend
hide_title: true
---
# Using a self hosted backend
## What you will learn in this guide
This guide will let you create a self hosted backend with an external database. This is an alternative to using an **Noodl Hosted Cloud Services**. Some reasons why you might want to do this could be:
- You want to host your own database, maybe to ensure where data is stored or other privacy reasons.
- You want to host your own backend on a cloud service such as AWS or Google Cloud Platform.
:::note
**Self Hosting**<br/>Note: To be able to follow this guide you must have "Self Host" feature enabled. This is not available in the free Noodl plan. But you can request a trial [here](https://noodl.net/plans).
:::
## Overview
We will go through the follwing steps
- Set up a Database cluster on MongoDB Atlas (any MongoDB or Postgres database is supported)
- Spin up a container with the Noodl backend docker image.
- Connect to the self hosted backend from your Noodl project.
## The different parts of a Noodl App
As a background it's good to know that a Noodl App consists of three parts:
- **The database** All Noodl applications must be backed by a database, you can use either a MongoDB or Postgres compatible database. This is where users and other records are stored. Nodes like **Query Records** access the database via the backend web service.
- **The backend service** This is the Noodl backend service that is provided via a Docker image and an instance can be started on most cloud providers. The Noodl backend is based on and compatable with the [Parse Platform](https://parseplatform.org) which is a great choice for a backend service. A solid open source project with an active foundation supporting many of the critical functions needed.
- **Static frontend hosting** Noodl applications are SPAs (Single Page Applications) and need a place that serves the application frontend created when you deploy your application from Noodl. You can use a **Noodl hosted** frontend, with a custom domain, together with a self hosted backend, or you can host the frontend yourself as well.
This guide will look at setting up your own self hosted **Database** and **Backend**.
## The Database
You can choose any MongoDB or Postgres compatible database, for this guide we recommend using [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) as it provides a free tier and you can choose to host it on GCP, AWS or Azure. You should always try to host your database using the same cloud provider as the backend service and preferably in the same region.
It's also very easy to get stated. Follow the instructions to setup your account. Create a new database.
<div className="ndl-image-with-background l">
![](/docs/guides/deploy/using-an-external-backend/atlas-1.png)
</div>
You can start by choosing the free plan (you can always upgrade later) and then the cloud providerand region where you would like host your database. Next you will be shown the security section of the setup. This controls who can access your database, you don't need to create a user as there is an admin user by default, you can choose to limit the IP-adresses that can access your database but don't worry about that now (you can always edit this later), just click **Finish and close**.
Once your database is up and running you need to get the connection details for the next step. First you need to obtain the password of the **Admin** user. You will find your database users under the **Database Access** section.
<div className="ndl-image-with-background m">
![](/docs/guides/deploy/using-an-external-backend/atlas-3.png)
</div>
Find the **Admin** user and clicked **edit**. Under the **Password** tab click **Edit Password**, generate a new password and copy it. Store it somewhere safe, you will need it for the next step. Don't forget to click **Update user** before moving on.
Now you need to find the connection URI. In your cluster dashboard choose _Connect_.
<div className="ndl-image-with-background l">
![](/docs/guides/deploy/using-an-external-backend/atlas-2.png)
</div>
Followed by **Connect to your application**.
<div className="ndl-image-with-background xl">
![](/docs/guides/deploy/using-an-external-backend/atlas-4.png)
</div>
Make sure the version is **3.6 or later**.
<div className="ndl-image-with-background l">
![](/docs/guides/deploy/using-an-external-backend/atlas-5.png)
</div>
Now you need to copy and keep the connection URI shown below. It will look something like this:
```bash
mongodb+srv://Admin:<password>@cluster0.xxxxxxx.mongodb.net/?retryWrites=true&w=majority
```
You need to replace the ```<password>``` with the **Admin** password you generated before. Also, insert the name of your database in the url, you can pick any name, let's call it `noodldb`, your final URL should look something like this:
```bash
mongodb+srv://Admin:<password>@cluster0.xxxxxxx.mongodb.net/noodldb?retryWrites=true&w=majority
```
Keep this URI safe as it will have full access to your database.
## The backend service
Next up we will deploy an instance of the Noodl backend service that we will point our application to. We provide guides for setting up Noodl on Amazon Web Services or Google Cloud Platform, follow the links below and set up the container. When you are ready you can proceed to the **Connect your application to the self hosted backend** below.
* **Setting up a backend on AWS** Make sure you have an AWS account created and then follow [this guide](/docs/guides/deploy/setting-up-backend-on-aws).
* **Setting up a backend on GCP** Make sure you have account on Google Cloud Platform created and then follow [this guide](/docs/guides/deploy/setting-up-backend-on-gcp).
### Connect your application to the self hosted backend
You can now connect to your new self hosted backend from your Noodl application. Open your project. Find the "Cloud Services" icon in the sidebar.
<div className="ndl-image-with-background l">
![](/docs/guides/deploy/using-an-external-backend/cloud-services.png)
</div>
Find the plus icon at the top to create a new cloud service.
<div className="ndl-image-with-background l">
![](/docs/guides/deploy/using-an-external-backend/cloud-services-2.png)
</div>
Create a new cloud service. Make sure the **Self Hosted** checkbox is checked:
<div className="ndl-image-with-background xl">
![](/docs/guides/deploy/using-an-external-backend/noodl-external-backend.png)
</div>
Fill out the information
- **Name** - Any name you want. This will be the name of the backend in the list of backends.
- **Description** - Some descriptive text of the backend.
- **Endpoint** - This is the url to the backend service you have created in the previous step.
- **Application id** - This is the application id that you provided when setting up the backend service in the previous step. It's simply and identifier of your own choosing.
- **Masterkey** - This is the master key to the backend service you created in the previous step. It is needed by the editor to access the database for the dashboard, query nodes etc. This is stored locally and encrypted. You need to keep this safe as with it you have full access to your backend and database.
You can make some quick tests, for example opening the **Dashboard** and create a **Class** to see that it works. That's it, now you have a self hosted Noodl cloud services up and running.
## Migrating from a Noodl hosted cloud service
If you are migrating from a Noodl hosted cloud service we can provide you with a database dump that you can use to restore your new database to, [email support to request](mailto:support@noodl.net). Once you have the backup file you need to install the MongoDB database tools, you can find instructions [here](https://www.mongodb.com/docs/database-tools/installation/installation/).
Then you will use the following command to migrate your data:
```bash
$ mongorestore --gzip --archive="path-to-backup-file" --uri="the-uri-to-your-mongodb-from-above"
```

View File

@@ -0,0 +1,68 @@
---
title: Key Bindings
hide_title: true
---
# Key Bindings
At the moment it is not possible to change the Shortcuts. The feature is on the roadmap to allow rebinding and potentially add shortcuts which we are not on by default.
:::info
Some of these shortcuts might change in future versions.
:::
## Keyboard Shortcuts Reference
<div className="ndl-table-35-65">
### Editor
| Key | Description |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| CtrlCmd + F | Focus the search panel allowing you to search in the project. |
| CtrlCmd + Shift + X | (Advanced) Refresh the viewer and node library, this is useful when working with modules. |
| CtrlCmd + Shift + E | (Advanced) Export Component. |
| CtrlCmd + Shift + P | Open Parse Dashboard for the active Cloud Service. |
| CtrlCmd + P | Open Parse Dashboard in the default browser for the active Cloud Service. |
| CtrlCmd + Shift + R | Open the DevTools for the cloud functions. |
| CtrlCmd + L | Focus the preview address bar. |
### Preview
| Key | Description |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| CtrlCmd + R | Refresh the preview window. |
| CtrlCmd + D | Open the DevTools for the preview window. |
| CtrlCmd + T | Toggle between Design and Preview mode for the preview window. |
### Node Graph
| Key | Description |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Space | Enter pan mode when down. |
| Arrow Up | Nudge the selected nodes up. |
| Arrow Down | Nudge the selected nodes down. |
| Arrow Left | Nudge the selected nodes left. |
| Arrow Right | Nudge the selected nodes right. |
| CtrlCmd + C | Copy the selected nodes. |
| CtrlCmd + V | Paste the selected nodes. |
| CtrlCmd + X | Cut the selected nodes. |
| CtrlCmd + Backspace | Delete the selected nodes. |
| CtrlCmd + Delete | Delete the selected nodes. |
| CtrlCmd + Z | Undo the last change. |
| CtrlCmd + Shift + Z | Redo the last change. |
| CtrlCmd + US_OPEN_SQUARE_BRACKET | Navigate back. |
| CtrlCmd + US_CLOSE_SQUARE_BRACKET | Navigate forward. |
| CtrlCmd + US_SLASH | Add a comment. |
| F1 | Open documentation for the currently selected node name. |
| Enter or F2 | Rename the currently selected node name. |
### Code Editor
| Key | Description |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| CtrlCmd + S | Save the code. (In some senarios this will also run the code) |
</div>

View File

@@ -0,0 +1,199 @@
---
title: Basic Navigation
hide_title: true
---
import ImportButton from '../../../src/components/importbutton'
# Basic Navigation
![](/docs/guides/navigation/basic-navigation/final.gif)
## What you will learn in this guide
In this guide you will be introduced to the [Page Router](/nodes/navigation/page-router), the [Page](/nodes/navigation/page) and the [Navigate](/nodes/navigation/navigate) node to create a simple UI where you can navigate between pages. You will learn how to navigate both by interacting with the UI or by entering URLs directly in the browser.
### Overview
We will go through the following steps in the tutorial
- Set up a **Page Router** with three **Pages**
- Configure the URL for the pages
- Implement navigation in the App
## What is Web Style Navigation?
Noodl supports two types of navigations: App style navigation and Web style navigation. This guide covers the Web style navigation using the **Page Router** and **Pages**.
On a high level the main differences are
- Web Style:
- Each Page has a distinct URL and by typing that URL in a browser window you will be routed to that page.
- You can encode data in the URL that can be extracted in the Page.
- The browser keeps track of your navigation stack, i.e. back history. The user can click back/forward in the browser window to navigate.
- Generally there are no transitions when moving between pages.
- App Style:
- The browser is unaware of the navigation - there is no change in URL when navigating.
- Noodl keeps track of the navigation stack.
- You can use different transitions when navigating.
You can mix the two navigation styles. For example a web page could have URLs leading in to the main section of the web site and within each section you use App style navigation.
When to use which style depends what the user expects from your app. Should it work like classic web-site or more like an app.
This guide will focus on Web Style navigation.
## The Page Router
The first node to look at in this guide is the **Page Router**. This node mainly keeps track of two things:
- Provide space for the **Page** that is currently showing
- Keep track of the other **Pages** you can navigate to
The **Page Router** node is a visual node meaning it takes up space on the screen. Any **Page** you navigate to will become child of the **Page Router** and use its space. So when designing your navigation flow, you typically create a bunch of **Page** components holding the content for each page and navigate between them. They will be put in the space that the **Page Router** takes up. All nodes in the visual hierarchy that are not in the **Pages** will stay on screen no matter how you navigate. So title and footer would normally not be part of the **Page** but lie next to the **Page Router** in the hierarchy.
Let's begin by creating a new project based on the "Hello World" Template. Delete the existing **Text** node and add in a **Page Router** using the Node Picker.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/basic-navigation/page-router-1.png)
</div>
Click on the **Page Router** to review its settings.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/basic-navigation/page-router-2.png)
</div>
For now - lets only change one thing: The background color to more clearly see the area on the screen that the page router takes up. Change it to a nice grey color.
As you can see, the whole screen changes color. It's because the **Page Router** by default takes up as much screen as it can on the screen.
Lets create a simple sidebar and add to the App. Since this sidebar is lying next to the **Page Router** in the hierarchy it will be a consistent component in the App no matter where you navigate. Navigation can only change what's in the **Page Router**.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/basic-navigation/ui-1.png)
</div>
The sidebar is 100 px wide and white. It's placed together with the **Page Router** in a [Group](/nodes/basic-elements/group) node with a horizontal layout.
## Pages
The next step is to add a few pages. The **Page** node is a bit special in the way that you cannot create it using the Node Picker. Instead you add it from the components sidebar as part of a component. Click "+" and the "Page Component". Call the page "Home".
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/basic-navigation/add-page-component.png)
</div>
Then add two more pages, "Products" and "Settings". Your component list should look as the image below.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/basic-navigation/components.png)
</div>
If you click one of the Page components you will se that it has two nodes by default. A **Page** node and a [Page Inputs](/nodes/navigation/page-inputs) node.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/basic-navigation/page-1.png)
</div>
Let's ignore the **Page Inputs** node for now. We will use it in later guides to send parameters to the **Pages**.
The **Page** node is a container that will hold the content you want to show. Let's add some content to it.
Add a **Group** node and then a [Text](/nodes/basic-elements/text) node as a child node. The **Text** will become the title of the page, so give it the name "Title" and center it. Also change the text so it matches the page you are changing, for example "Home" if you are in the Home page.
To more easily differentiate the pages from each other we will change the color of the Group to something that stands out, for example red.
Now change the other two pages, i.e. add the nodes, update the text title and change the color to a new color. You can select the two nodes and then copy the them using (Ctrl+C/Cmd+C) and paste them (Ctrl+P/Cmd+P) into the other pages.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/basic-navigation/page-2.png)
</div>
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/basic-navigation/screen.png)
</div>
As you change the pages you will see that one of them is already shown in the **Page Router**, the one that you added first. It has become the **Starting Page** of the **Page Router**. If you want to change that, you can go back to the Main App and click the **Page Router**. You will now see that there are two **Pages** added to your list of available **Pages** in the **Page Router**. You can hover over the **Pages** and a edit icon appears. It will open a menu where you can remove the page or make it the starting page. Try changing the starting page. You need to refresh the viewer for the changes to take place.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/basic-navigation/page-router-3.png)
</div>
## Page paths
Now, lets look at the paths to the pages. Go back to one of the **Page components** and click the **Page** node. You will se that it has a Title and a path that's based on the name of the component.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/basic-navigation/page-props.png)
</div>
For the case of the "Home" Page Component, it's path is "home". This means that the url to that page is `<your deployed domain>/home`. Let's try it with our viewer running on localhost.
Open a window in your favourite browser. Enter the URL `http://localhost:8574#/home`. Then try the paths to the other pages, for example `http://localhost:8574#/products`, `http://localhost:8574#/settings`. The URLs should lead you to the different pages.
## Navigating in the app
Finally lets add some navigation within the app. First step is to add buttons to the empty sidebar.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/basic-navigation/sidebar-1.png)
</div>
Change the buttons slightly to fit with the sidebar design, for example enabling the icon, removing the label and adding some padding and margins.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/basic-navigation/ui-2.png)
</div>
Finally we are going to connect this buttons to a [Navigate](/nodes/navigation/navigate) node, that will trigger the page navigation. So create three **Navigation** nodes. When you click them, you will see that you can change their target page to either one of the **Page Components** that are available in the **Page Router**.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/basic-navigation/navigate-1.png)
</div>
Make sure the three **Navigate** nodes points to the right target, and then connect the **Click** signal from the button to the **Navigate** signal on the **Navigate** nodes. Make sure the the correct button is connected to the correct **Navigate** node.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/basic-navigation/signal-1.png)
</div>
Now try clicking the buttons. If everything was set up correctly you should see the **Page Component** changing on the screen. Also try it in the browser and see how the URL changes. Try pressing the "Back"/"Forward" button in the browser and see how you move back and forth in the browser history.
If you want import the full project in Noodl click the "Import" button below and follow the instructions below.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/basic-navigation/final.gif)
<ImportButton zip="/docs/guides/navigation/basic-navigation/basic-navigation.zip" name="Basic Navigation" thumb="/docs/guides/navigation/basic-navigation/ui-2.png"/>
</div>

View File

@@ -0,0 +1,551 @@
---
title: Component Stack
hide_title: true
---
import ImportButton from '/src/components/importbutton';
import ReactPlayer from 'react-player';
# Component Stack Guide
## What you will learn in this guide
In this guide we will take a look at the [Component Stack](/nodes/component-stack/component-stack-node) node and the related navigation node [Push Component To Stack](/nodes/component-stack/push-component) and [Pop Component From Stack](/nodes/component-stack/pop-component).
They are an alternative navigation component compared to the [Page Router](/nodes/navigation/page-router) and related nodes. Instead of giving each **Page** a URL, and making use if the browser back history, **Component Stack** is completely handled from within the App. Both these styles of navigations can of course be combined. In this guide we will combine **Component Stack** with **Popups**. You will learn how to make a little modal wizard.
## Overview
The guide will cover the following topics
- Defining a component stack
- Pushing and popping components on the stack
- Forwarding data between components on the stack and outside the stack
- Keeping track of which component that is on top of the stack (and how many)
- Transitions when pushing and popping components
The guide is a continuation of the [Popup Guide](/docs/guides/navigation/popups) and it's recommended that you go through that guide first. It's also good to know the basics on data in Noodl, so also have a look at the [Object](/docs/guides/data/objects) and [Arrays](/docs/guides/data/arrays) guides before starting this guide. We will also use [Events](/docs/guides/business-logic/events) in the guide.
## Creating a modal Wizard using **Component Stack**
This guide will show you how to create a little "Wizard" type UI contained in a **Popup**. It's a continuation on the [Popup Guide](/docs/guides/navigation/popups). So start by importing the project below (by clicking the Import Button) into Noodl if you haven't finished that guide.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/popup-final.png)
<ImportButton
zip="/docs/guides/navigation/component-stack/popups.zip"
name="Popup Example"
thumb="/docs/guides/navigation/component-stack/popup-final.png"
/>
</div>
## Navigation within a Popup
To quickly recap what we achieved in the Popup Guide. We had a list of People and we created a Popup that had us confirm any deletion in the list. We also created a `Popup Base` component that we can use to make new nice looking Popups.
We now want to do a wizard style **Popup** were the user can add more persons to our list of people. "Wizard style" meaning the user will move through multiple screens within the **Popup** to create the entry. Normally, navigation in Noodl is done using the **Page Router** node (check out the guide [here](/docs/guides/navigation/basic-navigation)), but this it typically not meant for modal states of the app, since each **Page** have a unique URL. Instead you can use the [Component Stack](/nodes/component-stack/component-stack-node) node which doesn't affect the URL and therefore is more fit for a modal state in the app. It also supports transitions which is nice.
We want our "Add new Person" popup two have three states.
1. Fill out first name, last name and age of the new person to add to the list
2. Select country
3. Based on the country, select city
We also need to think about the data aspect. From the Popup guide we know that each person is stored in an object of this type:
```json
{
"firstname":<first name of the person>,
"lastname":<last name of the person>,
"age":<The age of the person>,
"location":<the city and country where the person lives>
}
```
The plan is the following:
1. When we open the popup, we create a new **Object** represening the new person
2. The first screen fills out `firstname`, `lastname` and `age` of the **Object**
3. The second screen the user selects their country
4. The third screen presents city options based on the country and then stores location in the **Object**
5. When the popup is closed, the new **Object** is added to the **Array**
Let's get started!
Start by creating a new Visual Component. Call it `Add New Person Popup`. Replace the **Group** node in it with a `Popup Base`. Change the title of it to "Add New Person".
As content in that **Popup** we are going to start with a **Text** node which will be consistent in the **Popup** that holds the current step of the wizard. ("Step x of 3"). So add a **String Format** node with the string `Step {current_step} of 3`.
Then we want our dynamic content that will change as the user progresses through the wizard. Create a **Component Stack** and add after the **Text** node.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/add-new-person-1.png)
</div>
## Adding components to a **Component Stack**
We are ready to create our components in our **Component Stack**.
:::note
When using a **Component Stack** we should not use **Page** components, since they are only to be used with **Page Routers**.
:::
Click on the **Component Stack**. Click the `+` under "Components" to add new components.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/component-stack/component-stack-1.png)
</div>
We want three components, call them `Step 1 - Name`, `Step 2 - Country`, `Step 3 - City`.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/component-stack/component-stack-2.png)
</div>
We are going to need to create components for each of these components. To keep better track of them we will put them in a folder of its own, so create a new component folder, `Create Person Wizard`. Create three visual components in the folder called `Step 1`, `Step 2` and `Step 3`.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/component-stack/folder-1.png)
</div>
You can now assign the components in the **Component Stack** in the `Add New Person Popup`. Click on it and set the components.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/component-stack/component-stack-3.png)
</div>
It's probably time that we add a **Button** to open the **Popup**. Go back to the main App. Add a horizontally centered **Button** with the label `Add New Person`. Also give it some top margin seperate it from the list. Then add a new **Show Popup** node. Connect the **Click** signal from the **Button** to the **Show Popup** signal **Show**. Also make sure to select the **Add New Person Popup** as the component to show.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/list-1.png)
</div>
Make sure the **Button** opens up the **Popup**.
Now let's work on the data part of the Wizard. We want to start by creating a new **Object** that we can fill out with data as the wizard progresses. In the `Add New Person Popup` we add a **Create New Object** node. We want to trigger the creation of the **Object** when the **Popup** is created. However we are not exposing the **Did Mount** signal on our `Popup Base` component, so we need to do that first. Open the `Popup Base` component, add a new property `Did Mount` on the **Component Outputs** and connect it to the **Did Mount** signal of the root **Group**.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/popup-base-1.png)
</div>
We can now connect the `Did Mount` signal from the `Popup Base` component to the **Do** signal of the **Create New Object** node in the `Add New Person Popup`, creating a new **Object** once the **Popup** opens.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/add-new-person-2.png)
</div>
## Sharing data betweem components in a **Component Stack**
This new **Object** is the one we will fill with data in the Wizard, so we need to be able to access it in our different subcomponents, `Step 1`, `Step 2` and `Step 3`. We could store the **Id** of the **Object** in a [Variable](/nodes/data/variable/variable-node) but an even neater solution is to use a [Component Object](/nodes/component-utilities/component-object). While **Variables** are global, the **Component Object** is only available to the children (and sub children) of the component where it resides, so there is no risk that someone accidently changes it where it shouldn't.
:::tip
The components on the **Component Stack** are considered children of the **Component Stack**, hence using **Component Objects** as a way to share data between different components on the **Component Stack** is often a good idea.
:::
So in the `Add New Person Popup`, create a **Component Object**. Give it a property `New Person Object Id`. Then connect the **Id** of the **Create New Project** to the new property of the **Component Object**.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/add-new-person-3.png)
</div>
We can now start creating our simplr form in the first component, `Step 1`. First, lets get our newly created **Object** set up. This time we use the [Parent Component Object](/nodes/component-utilities/parent-component-object) to retrieve it. Make sure to add the same property as before `New Person Object Id`. Then we can connect it to the **Id** of an **Object** node to retrieve the **Object**.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/step-1-1.png)
</div>
So let's add three [Text Input](/nodes/ui-controls/text-input) fields (for first name, last name and age). Hook them up to the **Object** (by adding the properties `firstname`, `lastname` and `age` - the last one being a number input only **Text Input**). Also add two **Buttons** one for "Cancel" and one for "Next" in a horizontal layout in a **Group**.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/step-1-2.png)
</div>
## Sending events to communicate outside the **Component Stack**
Left to do on this screen is to store the content of our **Text Inputs** in the **Object** once the user clicks "Next" and then go to the next step, `Step 2`. We also need to handle the "Cancel" click. Let's start with the latter.
When the user clicks "Cancel" we want to close the **Popup** but we have to do **Close Popup** in the component that was opened directly using a **Show Popup** (i.e. the `Add New Person Popup`). How do we notify that component that the user clicked "Cancel"? Again, since the component that's on top of the **Component Stack** can be seen as a child to the stack, we can [Send an Event](/nodes/events/send-event) to the parent component (for a dedicated guide on **Events**, see [here](/docs/guides/business-logic/events)).
So add a **Send Event** node and give the channel name `Cancel Add Person`. Make sure it's only directed towards the parent.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/component-stack/event-1.png)
</div>
Then trigger **Send** when the user clicks "Cancel".
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/step-1-3.png)
</div>
Go back to the `Add New Person Popup`. We need to add a [Receive Event](/nodes/events/receive-event) node here to react to the event. Make sure the `Cancel Add Person` channel is set. Also consume the event (set it to `Always`) to avoid the event propagating the tree - it's not needed beyond this component. Add a **Close Popup** node. Then we trigger the **Start Close Transition** signal on the `Base Popup` (to start the close transition) and then when the transition is done (the **Close Transition Done** signal is triggered), we trigger **Close** on the **Close Popup Node**.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/add-new-person-4.png)
</div>
## Pushing and replacing components on the **Component Stack**
Ok, now let's go back to the `Step 1` component to finish it off. First, we need to save the content of the **Text Inputs** in the **Object**. We do this by adding a **Set Object Properties** node, add our three properties (`firstname`,`lastname`, `age`). Also, don't forget to set the **Id** to the same **Id** we got from our **Parent Component Object**. Then we connect the **Click** signal on the "Next" **Button** to the **Do** signal on the **Set Object Properties**.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/step-1-4.png)
</div>
After saving we need to move to the `Step 2` component. Add a **Push Component To Stack** node. When you click on it, you can see a number of options. We see that it's using our `Main` (if we had more than one **Component Stack** in our project we would have to select the right one here). First let's change the **Target Component** to `Step 2 - Country`. We also see the **Mode** property. Here you can select **Push** or **Replace**. Since we want to be able to let the user go back in the Wizard in the each step, we probably want to keep **Push** as the mode. Essentially each component will be stacked on top of each other, and we can use the **Pop Component Stack** to go back to the previous component on the stack.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/component-stack/push-component-1.png)
</div>
We should trigger the **Navigate** signal on the **Push Component To Stack** when the data is stored, using the **Done** signal.
## Tweaking Transitions
Try out the navigation to the next step. You will see that the **Dark Overlay** option in the **Push Component To Stack** makes it looks weird so untick that option. Feel free to play around more with the transitions. Maybe change the **Shift Transition** to 100% to make it look more like a proper transition. You will see that it's important the the containing **Group** node clips its children for the transition to work. In the `Popup Base` component, that is the second **Group** that holds the **Component Children**.
## Editing `Step 2`
This will be a simpler form, containing a [Radio Button Group](/nodes/ui-controls/radio-button-group) with our country options. So let's add it in, together with three [Radio Buttons](/nodes/ui-controls/radio-button). We only have three options in this case, `England`, `Scotland` and `Ireland`. Make sure both **Label** and **Value** of the **Radio Buttons** are set to the respective value. Also add a sub title that says `Pick Country`. Finally, add two buttons at the bottom `Back` and `Next`. We can copy/paste them from the previous step and just change the title of the first **Button**.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/step-2-1.png)
</div>
## Popping from the **Component Stack**
We start by adding in a **Pop Component Stack** node. As you can see you can send results back in a similar way as the **Close Popup** (**Results** and **Back Actions**). In this case we don't have any data to pass. Instead just connect **Click** from the "Back" button to **Navigate**.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/step-2-2.png)
</div>
As you can see, popping from the stack plays the push transition, but in reverse. We can see that having different sizes of our components is a little bit annoying since the popup changes size dynamically as they are shown and hidden. Let's fix that by having a fixed size on the components `Step 1`, `Step 2` and `Step 3`.
We do this by going to our `Add New Parson Popup` and move in a new **Group** with a fixed size (250px / 300 px) as a parent to the **Component Stack**.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/add-new-person-5.png)
</div>
While we see that there is still a little layout-tweaking to be done (the buttons at the bottom moves around, etc). Let's fix that quickly. We want to make sure the buttons are always at the bottom of the screen. So find the **Group** containin the **Buttons** for `Step 1` and `Step 2`. Make sure to remove the **Margin** of the **Group**, or it will compete with the alignment, and then align it at the bottom.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/component-stack/group-alignment.png)
</div>
Great, now the buttons stay at the bottom!
Instead we want to add `Step 3` to the **Component Stack** when the user clicks "Next" in Step 2. We also need to pass on the selected country. We could of course store it in our **Object** but as you can see in the data model above, the country is not stored seperately. Instead we want to pass it on to `Step 3`. We could store it in the **Component Object**, as before, but in this case we can make use of the **Component Inputs** of the component to be opened.
Go to `Step 3` and add in a **Component Inputs** node. Add the port `Country`.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/component-stack/step-3-1.png)
</div>
<div className="ndl-image-with-background">
![](/docs/guides/navigation/component-stack/comp-input-1.png)
</div>
## Forwarding data to a component on the stack using **Component Inputs**
Now, if you go back to `Step 2`, and add in a **Push Component To Stack** and select `Step 3` as the target (also fix the transition as before), you will se that there is a new input called `Country`. We can connect the **Value** of the **Radio Button Group** to the `Country` input. Also connect **Click** on the "Next" **Button** to **Navigate** on **Push Component To Stack**.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/step-2-3.png)
</div>
Great! Let's move to `Step 3` then. Here we want to display options of cities depending on which country that was selected. Let's create a **Static Array** (set to JSON format) with the following data:
```json
[
{ "city": "Bristol", "country": "England" },
{ "city": "Hartlepool", "country": "England" },
{ "city": "Newcastle", "country": "England" },
{ "city": "Wigan", "country": "England" },
{ "city": "Kilkenny", "country": "Ireland" },
{ "city": "Dublin", "country": "Ireland" },
{ "city": "Cork", "country": "Ireland" },
{ "city": "Galway", "country": "Ireland" },
{ "city": "Glasgow", "country": "Scotland" },
{ "city": "Edinburgh", "country": "Scotland" },
{ "city": "Dundee", "country": "Scotland" },
{ "city": "Kildrummy", "country": "Scotland" }
]
```
We want to present a **Radio Button Group** with only the valid cities for the country that was selected in the previous step. So let's start with an [Array Filter](/nodes/data/array/array-filter).
Add a filter on the property `country`.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/component-stack/array-filter-1.png)
</div>
Make sure it's of the **Equals** type (and a String).
<div className="ndl-image-with-background">
![](/docs/guides/navigation/component-stack/array-filter-2.png)
</div>
Then connect **Items** of the **Static Array** to the **Items** of the **Array Filter**. Then connect the `country` output of the **Component Inputs** to the **Value** of the country filter.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/array-filter-3.png)
</div>
Add an **Array** to have a look at the results. We can now see that only the items that matches the country is filtered out. Nice!
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/step-3-2.png)
</div>
Now we need to visualize the option in a list, using a **Radio Button Group**, **Repeater** and **Radio Button**. Start by creating the list item. Call it `Step 3 List Item`. It should look like below:
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/step-3-list-item-1.png)
<CopyToClipboardButton
json={{
nodes: [
{
id: '5d3f90ec-abf3-7473-b43f-935e02141387',
type: 'Group',
x: -163,
y: -64,
parameters: { height: { value: 30, unit: 'px' } },
ports: [],
children: [
{
id: '331e1282-31df-46ad-73ca-69ba2b03f4bc',
type: 'net.noodl.controls.radiobutton',
x: -143,
y: -18,
parameters: { alignY: 'center' },
ports: [],
children: [],
},
],
},
{
id: 'b62673b4-2625-d20c-f473-81b537b7cc54',
type: 'Model2',
x: -575,
y: -92,
parameters: { properties: 'country,city', idSource: 'foreach' },
ports: [],
children: [],
},
{
id: '65ebe7fd-a47d-9edc-8e6d-dac435066879',
type: 'String Format',
x: -343,
y: 67,
parameters: { format: '{city}, {country}' },
ports: [],
children: [],
},
],
connections: [
{
fromId: 'b62673b4-2625-d20c-f473-81b537b7cc54',
fromProperty: 'prop-city',
toId: '331e1282-31df-46ad-73ca-69ba2b03f4bc',
toProperty: 'label',
},
{
fromId: 'b62673b4-2625-d20c-f473-81b537b7cc54',
fromProperty: 'prop-city',
toId: '65ebe7fd-a47d-9edc-8e6d-dac435066879',
toProperty: 'city',
},
{
fromId: 'b62673b4-2625-d20c-f473-81b537b7cc54',
fromProperty: 'prop-country',
toId: '65ebe7fd-a47d-9edc-8e6d-dac435066879',
toProperty: 'country',
},
{
fromId: '65ebe7fd-a47d-9edc-8e6d-dac435066879',
fromProperty: 'formatted',
toId: '331e1282-31df-46ad-73ca-69ba2b03f4bc',
toProperty: 'value',
},
],
}}
/>
</div>
The **Object** has two properties `country` and `city` and gets its **Id** from the **Repeater**. Also note that the **Value** of the **Radio Button** is using a **String Format** (with the string `{location}, {city}`) to construct the final location string that matches the data model (for example `Dublin, Ireland`).
Then we go back to `Step 3` component and add in a **Radio Button Group** and a **Repeater** (using our new `Step 3 List Item`) and then feed it with data. With some margin tweaking it will look something like below.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/step-3-3.png)
</div>
Ok, we continue by copying and pasting the **Buttons** from the previous step (we should really make these into a re-usable component!). Let's also bring the **Pop Component Stack** node as well (connected to the "Back" button) as we need one in this step as well.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/step-3-4.png)
</div>
## Saving the created **Object**
Ok we are almost done with our wizard. We need to save the location to the **Object** containing the info on the new person. We do it in the same way as in the `Step 1` component, we use a **Parent Component Object** to retrieve the **Object** that was created in the **Popup**. This time we store the **Value** of the **Radio Button Group** in a property called `location` We store the value when the user clicks "Next".
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/step-3-5.png)
</div>
Finally we need to let the `Create New Person Popup` know that the **Object** is filled out when the user clicks "Next" on the last stage. Again, we do that by sending an **Event** this time with the **Channel Name** `Confirm Add Person`, send it to **Parent**. We do that once the **Set Object Properties** is done.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/step-3-6.png)
</div>
## Passing data from the **Popup**
We go back to our `Add New Person Popup` to receive the event. Add in a **Receive Event Node** receiving from the channel `Confirm Add Person`.
### Event logic
We are going to need some extra logic here. Since both the `Cancel Add Person` and `Confirm Add Person` will trigger the close transition of the popup we need to be able to distinguish the two cases one the transition is ready. Because, in the `Confirm Add Person` case we want to pass the created **Object** to the main App, so it can be added to the list of People.
We add in a **Switch** node, that's only set to true if the `Confirm Add Person` event is received. The, once the transition is closed, we use a **Condition** node to test the value of the **Switch**. Depending on the case (i.e. **onTrue** or **onFalse** from the **Condition** node) we trigger two different **Close Actions** on the **Close Popup** node (`Confirm` or `Cancel`). We also make sure to pass the **Id** of the **Object** representing the person in a result called `New Person Object Id`.
Here's how the whole thing looks.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/add-new-person-6.png)
</div>
## Adding the **Object** to the **Array**
Now we need to receive the **Object** in our main App. We will get it through the `New Person Object Id` and add it if we get the `Confirm Add Person` signal from the **Popup**. We use the **Insert Object into Array** node to add it.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/list-2.png)
</div>
Try it out, we can now add new persons to our list using our little Wizard in a popup!
## Reading which component is on top of the **Component Stack**
In our Wizard we still have our text `Step x of 3`. We want to make that work. Go back to the `Add New Person Popup`. The **Component Stack** has an output called **Top Component Name** that will hold the name of the component that's currently on top Note that it's the name of the component in the **Component Stack**, not the component that's used to represent it. So in out case it will be `Step 1 - Name`, `Step 2 - Country`, `Step 3 - Location`. If we somehow could transfor this to the number 1,2 or 3, we can just feed it into our **String Format**. What might be an even easier solution is to use the **Stack Depth** property. Since we use **Push** rather than **Replace** our **Component Stack** will grow in size from 1 to 2 to 3, which is exactly what we want. So we connect it to the **String Format** node.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/component-stack/stack-depth.png)
</div>
## Making sure the data is filled out
We are almost done. There are many ways to improve this wizard but one obvious thing is that we want users to be forced to select something before moving to the next step. For example not selecting a country will mess up `Step 3`.
In `Step 1` we can look at the length of the **Text** value coming out of the **Text Input** nodes. We use the **String** node which has a **Length** output. We connect these three length values to an expression that makes sure all three of them are greater than zero (using the expression `a>0&&b>0&&c>0`). Then we connect the **Result** of that expression to the **Enabled** input of the "Next" **Button**.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/step-1-6.png)
</div>
In `Step 2` and `Step 3` we essentially do the same, but we look at the length of the **Value** of the **Radio Button Group**.
We are done! We have a small wizard in a Popup to fill information when adding new persons!
<ReactPlayer
playing
autoplay
muted
loop
url="/2.9/docs/guides/navigation/component-stack/component-stack-final.mp4"
/>
Click the Import button below to import the final project.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/component-stack/final-1.png)
<ImportButton
zip="/docs/guides/navigation/component-stack/component-stack-1.zip"
name="Wizard in Popup"
thumb="/docs/guides/navigation/component-stack/final-1.png"
/>
</div>

View File

@@ -0,0 +1,371 @@
---
title: Encoding Parameters in URLs
hide_title: true
---
import ImportButton from '../../../src/components/importbutton'
# Encoding Parameters In URLs
## What you will learn in this guide
This guide will teach you how to pass parameters in the URL when you are navigating between pages, either as the final part of a path, e.g. "mysite.com#/path/parameter" or as query strings, "mysite.com#/path?parameter1=val1&parameter2=val2".
Noodl is handling this through its [Page Inputs](/nodes/navigation/page-inputs) node and [Navigate](/nodes/navigation/navigate) node.
The main reason to encode data needed by a **Page Component** in URLs rather than using regular data nodes is that the data becomes independent of the App state. For example
- If a user refreshes the browser running the App, the data will still be available to the **Page Component** in the URL.
- If a user want to be able to share the exact state of an App with another user, you can encode that state in the URL.
## Overview
The guide will cover the following
- Pass data to a **Page Component** as a **Path Parameter**
- Navigate to a path and set the **Path Parameter**
- Pass data to a **Page Component** using a **Query Parameter**
The guide assumes that you are familiar with basic Web Navigation concepts and it's suggested that you have already gone through the [Basic Navigation](/docs/guides/navigation/basic-navigation) and [Multi Level Navigation](/docs/guides/navigation/multi-level-navigation) guides before you go through this guide.
We will also use the example app developed as part of the two guides as a starting point in this guide. If you haven't built it already, you can import it by clicking the button below and follow the instructions in Noodl.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/multi-level-navigation/multi-level-final.gif)
<ImportButton
zip="/docs/guides/navigation/multi-level-navigation/multi-level-navigation.zip"
name="Multi Level Navigation"
thumb="/docs/guides/navigation/multi-level-navigation/multi-level-thumb.png"
/>
</div>
## Path Parameters
To demonstrate a typical use of **Path Parameters** we will have to create a list of products to show in our app.
### Listing Products
The App will focus on Melee weapons.
In the main App component, create a [Static Array](/nodes/data/array/static-array) node. Make sure its set to `CSV` type. Edit the CSV data and paste in the data from below.
```
name,category,description,price,identifyer
Katana,steel,A classic japanese weapon used by the samurais,2000,katana
Broadsword,steel,A favorite among the medieval knights,1800,broadsword
Morning Star,steel,A scary and heavy weapon that require an expert to handle,1200,morning-star
Wooden Club,wood,A cheap and easy to use weapon that's popular among the farmers,50,wooden-club
Bokken,wood,A japanese wooden sword used in Kendo,850,bokken
Fake two handed sword,plastic,Looks real but so much lighter than the real thing,400,fake-sword
Mace for practice,plastic,A plastic mace you can use for training,250,plastic-mace
```
Feel free to add more data to the dataset if you want, as long as the category is one of "steel"/"wood"/"plastic". Also make sure the the "identifyer" value of any new row you add does not contain any whitespaces.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/encoding-parameters-in-urls/static-array.png)
</div>
Ok, now lets list the products under the Products page with their category, i.e. any "steel" products should be listed on the products/steel page.
First we put the Products in an Array that we can find anywhere in the app. Create an [Array](/nodes/data/array/array-node) node next to the **Static Array**. Give it the **Id** `Products`. Then connect the output **Items** on the **Static Array** to the input **Items** on the **Array**.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/encoding-parameters-in-urls/array-1.png)
</div>
Now we will go into the "Steel" Product Page and list the products of the "steel" category. We will use a [Repeater](/nodes/ui-controls/repeater) and create a list item.
In the steel page, add a **Repater** node as the second child of the group. Also add an **Array** node. Make sure its **Id** is `Products`, i.e. the same array that we filled with the product data. Then connect the **Items** output of the **Array** connects to the **Items** input of the **Repeater**.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/encoding-parameters-in-urls/products-page-1.png)
</div>
We will still not see anything because we have no list item that the **Repeater** can use to visualize the items. So lets create a new component, we call it "Product Item". In the new component we add a **Text** node as the only child in the **Group**. Now we need to retrieve the individual **Object** that contains the Product entry. Create an [Object](/nodes/data/object/object-node) node and make sure it gets its **Id** from the repeater node. Also add a property "name" to the **Object**.
<div className="ndl-image-with-background m">
![](/docs/guides/navigation/encoding-parameters-in-urls/id-repeater.png)
</div>
<div className="ndl-image-with-background m">
![](/docs/guides/navigation/encoding-parameters-in-urls/object-1.png)
</div>
Finally hook up the **name** output of the **Object** with the **text** property of the **Text** node. We will have something like image below.
<div className="ndl-image-with-background m">
![](/docs/guides/navigation/encoding-parameters-in-urls/list-item-1.png)
</div>
Now we are ready to go back to the "Steel" page and select our newly created List Item as the List Item of the repeater.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/encoding-parameters-in-urls/repeater-1.png)
</div>
If you navigate to the "Steel" page, you should now see a bunch of items popping up. It should look something like this:
<div className="ndl-image-with-background">
![](/docs/guides/navigation/encoding-parameters-in-urls/list-1.png)
</div>
We immediately see two things
- The list items need to be styled
- We see all products, not only the ones in the "steel" category
To fix the styling we go back into the "Product Item" component, make sure the **Group** node is **Content Height** (so the list gets a bit more compact). We also add a **Hover State** on the **Text** node. There is plenty of more styling that can be done, but lets settle for now.
Secondly, to filter out the products of the "steel" category, we go back to the "Steel Products" Page Component.
We add in a [Array Filter](/nodes/data/array/array-filter) node in between the **Array** and **Repater**.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/encoding-parameters-in-urls/products-page-2.png)
</div>
And finally we configure the **Array Filter** to only show items where `category = "steel"`.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/encoding-parameters-in-urls/array-filter-1.png)
</div>
<div className="ndl-image-with-background">
![](/docs/guides/navigation/encoding-parameters-in-urls/array-filter-2.png)
</div>
Now your Steel Page should look something like the one below.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/encoding-parameters-in-urls/list-2.png)
</div>
Quickly copy-and-paste the **Array**+**Array Filter**+**Repeater** construction to the "Wood" and "Plastic" page, make sure to update the filter, and we are done with listing the products.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/encoding-parameters-in-urls/sorting.gif)
</div>
### Adding the Show Product Page
Next step is to add a new **Page Component** to the main **Page Router**. We want to have it mapped to the URL `/showproduct`. Click the main **Page Router** (in the App component), the click **Add New Page** and **Create New Page**.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/encoding-parameters-in-urls/create-new-page-1.png)
</div>
<div className="ndl-image-with-background">
![](/docs/guides/navigation/encoding-parameters-in-urls/create-new-page-2.png)
</div>
Lets first go to the new page and make sure the path is "showproduct" by clicking the **Page** node and updating the path.
So how do we know which of the Melee Weapon Products to show here? Well, that's where the **Path Parameter** comes in.
### Using a Path Parameter
We want to send the `identifyer` value of the a clicked product as a **Path Parameter** and use that to look up the correct product. I.e. we want to Navigate to this page with the path `/showproduct/<identifyer>`.
We achieve that by clicking the **Page Inputs** node and add a **Path Parameter** called "productIdentifyer". The name doesnt matter except that becomes the name of the output of the **Page Inputs** node that will contain whatever is sent in the last part of the path.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/encoding-parameters-in-urls/path-param-1.png)
</div>
For test purposes, add a **Text** node to the **Page** node and connect the "productIdentifyer" output to the **Text** input of the **Text** node.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/encoding-parameters-in-urls/connect-text.png)
</div>
## Set the Path Parameter when Navigating
Ok, we are almost ready to try the "showproduct" Page. We just need to do the actual navigation.
So lets go back to the "Product Item" component we build earlier. We want to navigate when we click these items.
Add a **Navigate** node and make sure the associated **Page Router** is "Main". Then pick the "Show Product Page" as the target.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/encoding-parameters-in-urls/navigate-1.png)
</div>
As soon as you select it, you will see that there is a new input called "productIdentifyer". This is the one we added on the **Page Inputs** earlier!
We want to set it to the "identifyer" value from our product so lets add the property "identifyer" on our **Object** and connect it to the "productIdentifyer" input of our **Navigate** node. Also, connect the **Click** signal from the **Group** node to the **Navigate** signal on the **Navigate** node.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/encoding-parameters-in-urls/list-item-2.png)
</div>
Now try clicking on different products under the different Products pages. You should see the "Show Product Page" with the corresponding identifyer printed on the screen. Also try it in a regular browser to see how the URL looks.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/encoding-parameters-in-urls/browser-1.png)
</div>
### Prettifying the "Show Product Page"
To finish up the "Show Product Page" we will extract some real data from the product and present in a sligthly prettier way. Go to the "Show Product Page".
Add a new **Group** under the **Page** node and remove the **Text** node that was previously there. Make the **Group** white and give it some margin to give it some space. Add rounded corners and give it some padding for what's going inside it.
Then add three **Text** nodes, one for "name", one for "description" and one for "price". Make the first **Text** node bold. Also, add some nice margins in-between them.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/encoding-parameters-in-urls/show-products-page-1.png)
</div>
Now we need to connect the data. We have the "identifyer" coming in as our **Path Parameter**. We are going to use that to filter out the right **Object** from our "Products Array".
So we connect it to an **Array Filter**, which we filter on "identifyer". We should only get one item out, so we can take the **First Item Id** and connect it to the **Id** of an **Object** node.
The **Object** should have three properties, "name", "description" and "price". The "price" need to be formatted. So add a [String Format](/nodes/string-manipulation/string-format) node with the string `Price: {price} EUR`.
An input "price" should now be available on the **String Format** node. Connect the "price" output from the **Object** to it.
The data extraction should look similar to below:
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/encoding-parameters-in-urls/show-products-page-2.png)
</div>
Then connect the data to the **Text** nodes and we are done with this part.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/encoding-parameters-in-urls/show-products-page-3.png)
</div>
<div className="ndl-image-with-background m">
![](/docs/guides/navigation/encoding-parameters-in-urls/show-products-page-4.png)
</div>
## Using Query Parameters
Another way to send parameters to pages is as _Query Parameters_. While you can only have one **Path Parameter**, you can have as many **Query Parameters** as you want.
Lets add a **Query Parameter** to our **Page Inputs** node. We will call it `showVAT` and we will use it to determine whether the price should include VAT or not in the **Show Products Page**.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/encoding-parameters-in-urls/query-param-1.png)
</div>
Now lets make use of the parameter. We start by going to the "Settings" page component. We will let the user state its preference here, whether they want to include VAT or not in the displayed price.
### Adding a VAT setting
Add a [Radio Button Group](/nodes/ui-controls/radio-button-group) with a **Text** node and two [Radio Buttons](/nodes/ui-controls/radio-button) as children. The text will be the title for the group. Make sure the first **Radio Button** have the value `showVAT` and the second `dontShowVAT`.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/encoding-parameters-in-urls/settings-1.png)
</div>
<div className="ndl-image-with-background">
![](/docs/guides/navigation/encoding-parameters-in-urls/settings-2.png)
</div>
So lets save the VAT state, using a bit of logic and a [Variable](/nodes/data/variable/variable-node) / [Set Variable](/nodes/data/variable/set-variable) node. Note that we connect the **Variable** that holds the current value to the **Value** of the **Radio Group** node to make sure it reflects the current setting when navigating to the **Settings** Page. (Remember that the **Page Component** will be re-instanciated every time you navigate to it so we need to set the initial value every time.)
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/encoding-parameters-in-urls/settings-3.png)
</div>
Now we need to update our navigation to encode the setting in the URL. But maybe we should revisit why we would want to do that. Why not use our **Variable** `Show VAT` directly in the **Show Products** page component? Well, if we want our users to be able to copy the URL they are on when looking at a product, and we want the receiver to see the exact same view as the sender, we need to encode the setting in the URL.
### Sending the Query Parameter
Go to the "Product Item" component where we do the navigation to the "Show Product" Page. If you click the **Navigate** node you will now see that the `showVAT` parameter is availabel as an input.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/encoding-parameters-in-urls/navigate-2.png)
</div>
We simply connect our **Variable** to it and the value will be sent.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/encoding-parameters-in-urls/list-item-3.png)
</div>
### Receiving the Query Parameter
The only thing left is to receive the parameter in the "Show Product" Page and show different prices depending on the parameter. We add a **Variable** that will hold the string to be shown (the price string including the VAT or not). Then we add a [Condition](/nodes/utilities/logic/condition) node to set the correct string to the **Variable**. The nodes look as below:
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/encoding-parameters-in-urls/list-item-4.png)
</div>
If you want to download the complete project then press the "Import" button below and follow the instructions in Noodl.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/encoding-parameters-in-urls/encoding-params-final.gif)
<ImportButton
zip="/docs/guides/navigation/multi-level-navigation/param-encoding-url.zip"
name="Encoding Parameters in URL"
thumb="/docs/guides/navigation/multi-level-navigation/show-products-page-4.png"
/>
</div>

View File

@@ -0,0 +1,218 @@
---
title: Multi Level Navigation
hide_title: true
---
import ImportButton from '../../../src/components/importbutton'
# Multi Level Navigation
## What you will learn in this guide
In this guide we will look at using multiple [Page Routers](/nodes/navigation/page-router) to achieve navigation hierarchies with multiple levels. We will use the [Navigate](/nodes/navigation/navigate) and [Navigate To Path](/nodes/navigation/navigate-to-path) nodes to move between the different **Pages** of the App.
This is used when your **Pages** are routed to URLs where each subroute manages their own routes. For example you may have a section of your app, "Products", that you reach through the URL `https://mydomain.com/products`, which in turn has three subsections:
- `/products/steel`
- `/products/wood`
- `/products/plastics`
There's another section, "Settings", with the URL `https://mydomain.com/settings`. It has two subsections:
- `/settings/company`
- `/settings/user`
## Overview
We will go through the following steps:
1. Add and configure multiple layers of **Page Routers**.
2. Use the **Navigate** node to move between pages within each **Page Router**.
3. Use the **Navigate To Path** to navigate in multiple **Page Routers** in one go.
If you are new to Web Type Navigation you should go through the [Basic Navigation](/docs/guides/navigation/basic-navigation) guide first.
We will build upon the example built in that guide. If needed you can first import the base project by clicking the "import" button below.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/multi-level-navigation/prev-final.gif)
<ImportButton
zip="/docs/guides/navigation/multi-level-navigation/basic-navigation.zip"
name="Basic Navigation"
thumb="/docs/guides/navigation/multi-level-navigation/ui-2.png"
/>
</div>
## Multiple Page Routers
The first thing we want to add to our app is three categories under our "Product" page. We want three categories
- Steel Products - with the path `/products/steel`
- Wood Products - with the path `/products/wood`
- Plastic Products - with the path `/product/plastic`
We also want a Top Bar Menu, only available in the Products Page, to navigate between them.
How you acheive this in Noodl is to add a **Page Router** in the Products Page, and create three new pages for the sub pages. We add the Top Bar directly in the Products Page, so it will be consistent between the three subpages.
So lets start with creating the Top Bar. Go to the Product Page.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/multi-level-navigation/component-list-1.png)
</div>
<div className="ndl-image-with-background">
![](/docs/guides/navigation/multi-level-navigation/products-page-1.png)
</div>
More or less the same way as the sidebar menu was created, we can create the Top Bar. The main difference is that its laid out horizontally and that the buttons has a text instead of an icon. Make sure the Top Bar Group is set to **Content Height** so it doesnt take upp any unnecessary space.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/multi-level-navigation/products-page-2.png)
</div>
<div className="ndl-image-with-background s">
![](/docs/guides/navigation/multi-level-navigation/top-bar-panel.png)
</div>
Now let's add a **Page Router** under the Top Bar. It will take up the rest of the space of the **Page**. We change the name of the **Node** and the name of the **Page Router** (the **Name** Property) to "Products Router".
<div className="ndl-image-with-background">
![](/docs/guides/navigation/multi-level-navigation/page-router-1.png)
</div>
<div className="ndl-image-with-background">
![](/docs/guides/navigation/multi-level-navigation/products-page-3.png)
</div>
The next step is to add three new **Page Components** using the "+" icon on the Components List. To keep things tidy, we first create a new folder, "Products Pages", and add the **Page Components** under that folder, "Steel Page", "Wood Page", "Plastic Page"
As you can see you now have to select which **Page Router** you want to add the **Page Component** to. We choose our newly created "Products Router".
<div className="ndl-image-with-background">
![](/docs/guides/navigation/multi-level-navigation/add-page.png)
</div>
Once each page is created, add a **Group** node with some easy to recognize color and add a **Text** node as a title, centered.
A fast way to create the **Page Components** is to create one, and the use the "Duplicate" component feature.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/multi-level-navigation/duplicate-page.png)
</div>
After creating the pages, changing their color and title, you should now have something that looks like this.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/multi-level-navigation/pages-1.png)
</div>
You can now double check the **Page Router** in the "Products Page". It should have the three newly created pages as their available **Page Components**.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/multi-level-navigation/page-router-2.png)
</div>
We should also check the individual **Page** nodes so the "Url Path" is set correctly. Change it to "steel", "wood", "plastic".
<div className="ndl-image-with-background">
![](/docs/guides/navigation/multi-level-navigation/page-path-1.png)
</div>
With the right URL Paths set in place you should now be able to navigate directly to the inner pages of the "Products Page" by editing the URL in the navigation bar of the browser.
Open your favourite browser and try the three different URLs `http://localhost:8574/#/products/steel`, `http://localhost:8574/#/products/wood`, `http://localhost:8574/#/products/plastic`. They should tell the Main **Page Router** to go to the "Products Page" and then the Products Page **Page Router** to go to the specific pages, "Steel", "Wood" or "Plastic".
## Navigating Specific Routers
Now lets hook up the Top Bar. Create three **Navigate** nodes in the Products Page Component. Set each of them to target the **Products Router** **Page Router** and chose the respective target page.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/multi-level-navigation/topbar-navigate.png)
</div>
<div className="ndl-image-with-background">
![](/docs/guides/navigation/multi-level-navigation/products-router-navigate.png)
</div>
Try clicking the Top Bar. You should now be able between the **Page Components** in the inner **Page Router**.
## Using the Navigate To Path node
Finally, lets add a shortcut to the "Plastic" page to the sidebar. Clicking that button should do two things:
- Navigate to the "Products" **Page Component** in the Main **Page Router**.
- Navigate to the "Plastic" **Page Component** in the Products **Page Router**.
Doing this using the individual **Page Routers** would be a bit messy, so instead we use the **Navigate To Path** node. It will give the App a path to navigate to and let **Noodl** resolve which navigations that need to happen on which **Page Routers** based on the path.
First lets add the shortcut button. Go to the main App component and add a new **Button** to the sidebar. Call it "Shortcut: Plastic Products". Make it red so it stands out, align it to the bottom, and change the Text Style to a smaller font. Something like this:
<div className="ndl-image-with-background">
![](/docs/guides/navigation/multi-level-navigation/shortcut-1.png)
</div>
<div className="ndl-image-with-background">
![](/docs/guides/navigation/multi-level-navigation/main-app-1.png)
</div>
To finish things up, add a [Navigate To Path](/nodes/navigation/navigate-to-path) node, make sure its path is set to `products/plastic`.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/multi-level-navigation/navigate-to-path.png)
</div>
Finally connect the **Click** signal of the **Button** to **Navigate** on the **Navigate To Path** node and we are done.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/multi-level-navigation/main-app-2.png)
</div>
Try clicking the Shortcut Button and make sure it always navigates to the "Products" **Page Component** and the "Plastic" **Page Component** within it.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/multi-level-navigation/multi-level-final.gif)
<ImportButton
zip="/docs/guides/navigation/multi-level-navigation/multi-level-navigation.zip"
name="Multi Level Navigation"
thumb="/docs/guides/navigation/multi-level-navigation/multi-level-thumb.png"
/>
</div>

View File

@@ -0,0 +1,14 @@
---
title: Navigation in Noodl Overview
hide_title: true
---
import ReactPlayer from 'react-player'
# Navigation in Noodl
A central part of any App is a navigation system. Noodl has a super flexible navigation system where you can build both classic Web Style navigation, app style navigation and Popups.
<ReactPlayer playing autoplay muted loop url='overview/signup.mp4' />
### [Start learning about navigation](/docs/guides/navigation/basic-navigation)

View File

@@ -0,0 +1,375 @@
---
title: Popups
hide_title: true
---
import CopyToClipboardButton from '/src/components/copytoclipboardbutton'
import ImportButton from '/src/components/importbutton'
# Popups
## What you will learn in this guide
This guide will teach you how to use Popups in Noodl (represented by the [Show Popup](/nodes/popups/show-popup) and [Close Popup](/nodes/popups/show-popup) nodes). Popups are modal UI components that's put on top of the regular UI flow i.e. you have to close them to return to the main flow. They are typically used to present confirmation dialogies ("Save / Cancel?") or error messages. They can also be used to present inline UI flows, for example wizards and forms, that need to be completed as part of another flow.
## Overview
The guide will cover the following topics
* Simple styling of Popups
* Opening and closing Popups and sending the result of a popup
* Advanced styling of Popups
* Making a reusable, extendible Popup component
The example we are building in the guide will make use of lists so it's recommended to have done the [List Basics](/docs/guides/data/list-basics) guide before starting this guide.
## What's a Popup in Noodl?
Basically any component you create can be opened as a Popup in Noodl. A Popup is layered on top of your regular UI that originates from the root node of your App. Also, when the Popup is open, you cannot interact with any of the control in the regular tree.
Let's try it out.
## Some initial UI
We are going to create a small App to add Popups into. Start a new project in Noodl and select the "Hello World" template. Then remove the **Text** node. We want to create a list of people. Let's start with some static data. Add in a [Static Array](/nodes/data/array/static-array) node. Make sure it's set to `JSON` type and paste in the data below.
<div className="ndl-image-with-background xl">
![](/docs/guides/navigation/popups/list-1.png)
</div>
```json
[
{"firstname":"John", "lastname":"Barry", "age":24, "location":"Bristol, England"},
{"firstname":"Stefani", "lastname":"Worthington", "age":41, "location":"Kilkenny, Ireland"},
{"firstname":"Lisa", "lastname":"Hunter", "age":22, "location":"Wigan, England"},
{"firstname":"Ronan", "lastname":"Holden", "age":56, "location":"Aberdeen, Scotland"},
{"firstname":"Martin", "lastname":"McCarthy", "age":26, "location":"Glasgow, Scotland"},
{"firstname":"Rodney", "lastname":"O'Hara", "age":53, "location":"Dublin, Ireland"},
{"firstname":"Phil", "lastname":"Lineker", "age":21, "location":"Hartlepool, England"}
]
```
Then add in a [Repeater](/nodes/ui-controls/repeater) node under the root **Group** node. Also add an [Array](/nodes/data/array/array-node) node next to the **Static Array**. Give the **Array** the **Id** `People`. Finally connect the **Item** output of the **Static Array** to the same input on the **Array**. Then the **Items** output from the **Array** to the **Items** input of the **Repeater**.
Your main app should now look something like this:
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/popups/list-1.png)
</div>
We need to create a list item as well. Create a new visual component. Call it `List Item`.
Do a simple layout visualizing the data items in the **Static Array**. You can for example copy and paste the nodes below into the list item component.
<div class="ndl-image-with-background l">
![](/docs/guides/navigation/popups/list-item-1.png)
<CopyToClipboardButton json={{"nodes":[{"id":"563a9961-1e16-62e4-10e1-2a97fba28687","type":"Group","x":-251,"y":-58,"parameters":{"sizeMode":"explicit","height":{"value":40,"unit":"px"},"flexDirection":"row"},"ports":[],"children":[{"id":"1ce9f404-33ef-5efc-d514-9c7f7105c321","type":"Text","label":"First Name","x":-231,"y":44,"parameters":{"width":{"value":25,"unit":"%"},"alignY":"center"},"ports":[],"children":[]},{"id":"4f627f54-67be-9f43-de8f-239f7d2cc788","type":"Text","label":"Last Name","x":-231,"y":140,"parameters":{"width":{"value":25,"unit":"%"},"alignY":"center"},"ports":[],"children":[]},{"id":"8b2dd25c-0ff3-f3db-0c2f-8198d0feaece","type":"Text","label":"Age","x":-231,"y":236,"parameters":{"width":{"value":10,"unit":"%"},"alignY":"center"},"ports":[],"children":[]},{"id":"0b069cd5-68c3-1a18-5962-561316fa3390","type":"Text","label":"Location","x":-231,"y":332,"parameters":{"width":{"value":30,"unit":"%"},"alignY":"center"},"ports":[],"children":[]},{"id":"493b675a-33d2-85df-754f-5d540765aa44","type":"Group","x":-231,"y":428,"parameters":{"sizeMode":"explicit","width":{"value":10,"unit":"%"}},"ports":[],"children":[{"id":"8b7ae2ea-9f7e-f3c5-b364-8527bd0928d3","type":"net.noodl.controls.button","label":"Delete button","x":-211,"y":474,"parameters":{"useIcon":true,"sizeMode":"contentSize","alignX":"center","alignY":"center","useLabel":false,"iconIconSource":{"class":"material-icons","code":"delete"},"width":{"value":10,"unit":"px","isFixed":false},"height":{"value":20,"unit":"px","isFixed":false},"paddingLeft":{"value":4,"unit":"px"},"paddingRight":{"value":4,"unit":"px"},"paddingTop":{"value":4,"unit":"px"}},"ports":[],"children":[]}]}]},{"id":"54762794-37ba-54a4-22fc-f33b3b2e4e18","type":"Model2","x":172,"y":28,"parameters":{"idSource":"foreach","properties":"firstname,lastname,age,location"},"ports":[],"children":[]},{"id":"4311a736-5a74-4961-0910-e29325bc4421","type":"Component Outputs","x":318.62192606679525,"y":320.6984274555043,"parameters":{},"ports":[{"name":"Request Delete","plug":"input","type":{"name":"*"},"index":1}],"children":[]},{"id":"09a3e9af-aaf1-3e7d-e293-f9e544a89290","type":"Switch","x":101.58788062761681,"y":250.0541174251976,"parameters":{},"ports":[],"children":[]}],"connections":[{"fromId":"54762794-37ba-54a4-22fc-f33b3b2e4e18","fromProperty":"prop-firstname","toId":"1ce9f404-33ef-5efc-d514-9c7f7105c321","toProperty":"text"},{"fromId":"54762794-37ba-54a4-22fc-f33b3b2e4e18","fromProperty":"prop-lastname","toId":"4f627f54-67be-9f43-de8f-239f7d2cc788","toProperty":"text"},{"fromId":"54762794-37ba-54a4-22fc-f33b3b2e4e18","fromProperty":"prop-age","toId":"8b2dd25c-0ff3-f3db-0c2f-8198d0feaece","toProperty":"text"},{"fromId":"54762794-37ba-54a4-22fc-f33b3b2e4e18","fromProperty":"prop-location","toId":"0b069cd5-68c3-1a18-5962-561316fa3390","toProperty":"text"},{"fromId":"8b7ae2ea-9f7e-f3c5-b364-8527bd0928d3","fromProperty":"onClick","toId":"4311a736-5a74-4961-0910-e29325bc4421","toProperty":"Request Delete"},{"fromId":"563a9961-1e16-62e4-10e1-2a97fba28687","fromProperty":"hoverStart","toId":"09a3e9af-aaf1-3e7d-e293-f9e544a89290","toProperty":"on"},{"fromId":"563a9961-1e16-62e4-10e1-2a97fba28687","fromProperty":"hoverEnd","toId":"09a3e9af-aaf1-3e7d-e293-f9e544a89290","toProperty":"off"},{"fromId":"09a3e9af-aaf1-3e7d-e293-f9e544a89290","fromProperty":"state","toId":"8b7ae2ea-9f7e-f3c5-b364-8527bd0928d3","toProperty":"visible"}]}} />
</div>
Then set the newly created List Item component as the Template in the **Repeater** node.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/popups/repeater-1.png)
</div>
You will now have a list of people that looks something like this:
<div className="ndl-image-with-background">
![](/docs/guides/navigation/popups/app-1.png)
</div>
Now let's implement delete on this list. The delete button is already there. When the user clicks the delete button we want to trigger the [Remove Object From Array](/nodes/data/array/remove-from-array) node, with the **Id** of the `People` **Array** and the id of the **Object** to remove fed into it. There is a [specific guide on Arrays](/docs/guides/data/arrays) if you want to learn more about how to manipulate **Arrays**.
The remove functionality is implemented like below in the main App.
<div className="ndl-image-with-background xl">
![](/docs/guides/navigation/popups/list-2.png)
</div>
Try clicking the delete button on some items. If you refresh the viewer, they will re-appear, since they are stored in a **Static Array**.
## Implementing a Confirm Popup
A typical case to use a Popup is to confirm a destructive action such as a deletion. So let's create a Popup for that.
Create a new visual component. Call it "Confirm Delete Popup". Then, also in the main App, create an **Show Popup** node. Connect the `Request Delete` signal from the **Repeater** to the **Show** signal of the **Show Popup** node. Clicking the delete button will now open our Popup. However we need to tell the **Show Popup** node which component to show. Select the newly createad "Confirm Delete Popup" component.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/popups/popup-panel-1.png)
</div>
<div className="ndl-image-with-background xl">
![](/docs/guides/navigation/popups/list-3.png)
</div>
## Layout and visuals of a Popup
Go into it the Popup component. Popups are layed out in an individual layer on top of everything, so you can more or less see it as a new root node.
### The Popup Background
We start by modifying the root **Group** node. This is typically the background of the whole Popup layer, i.e. you probably would want to make this cover the whole screen and perhaps make semi transparent. We cannot use the **Opacity** to make it transparent, because that will be inherited by all the children of the **Group** as well. Instead we chose a semit transparent color. It will sort of fade out what's behind the popup.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/popups/popup-visuals-0.png)
</div>
### The Popup Dialogue
The next thing to build is the dialogue. We want it to be nice and rounded. Let's add another **Group** node. Give it a white color. Also make the corners rounded, with a grey color and a corner radius of 20 pixels.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/popups/popup-visuals-1.png)
</div>
The size of the popup is interesting. We want the size to be dependen on its content. At the same time we don't want it to be too small or too wide horizontally. So we set its size to `content size` and center it horizontally and vertically.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/popups/popup-visuals-2.png)
</div>
Then and then go ahead set
* **Min width = 200 px**
* **Max width = 400 px**
* **Min height = 100 px**
* **Max height = 100 px**
<div className="ndl-image-with-background">
![](/docs/guides/navigation/popups/popup-visuals-3.png)
</div>
Finally add some padding, 15 px in all directions.
The popup should now look something like this:
<div className="ndl-image-with-background xl">
![](/docs/guides/navigation/popups/popup-visuals-4.png)
</div>
### Adding some content
So let's add some content. A **Text** node which is a title, (i.e. give it a larger font size, perhaps 30 px and with centered text). Then another smaller **Text** with the actual deletion message.
Finally add a **Group** that will contain the two buttons, layed out horizontally.
Add the two buttons (one green and one red). Align them to the left and right. Finally add some margins to make things look less constricted.
You popup will now look something like below. You can also copy and paste the nodes if you want this exact look.
<div class="ndl-image-with-background xl">
![](/docs/guides/navigation/popups/popup-visuals-5.png)
<CopyToClipboardButton json={{"nodes":[{"id":"74711a71-f264-654c-9855-2aa66a25f0e0","type":"Group","x":-336,"y":-142,"parameters":{"backgroundColor":"#FFFFFFB2"},"ports":[],"children":[{"id":"14a19f8f-22f0-5196-9813-c34b47a000d8","type":"Group","label":"Group","x":-316,"y":-96,"parameters":{"sizeMode":"contentSize","alignX":"center","alignY":"center","paddingTop":{"value":15,"unit":"px"},"paddingBottom":{"value":15,"unit":"px"},"paddingRight":{"value":15,"unit":"px"},"paddingLeft":{"value":15,"unit":"px"},"borderRadius":{"value":20,"unit":"px"},"borderStyle":"solid","backgroundColor":"#FFFFFF","borderColor":"#484545","minWidth":{"value":200,"unit":"px"},"maxWidth":{"value":400,"unit":"px"},"minHeight":{"value":100,"unit":"px"},"maxHeight":{"value":400,"unit":"px"}},"ports":[],"children":[{"id":"75b58ecf-48ab-dbad-e4ca-0f6a7e077f23","type":"Text","label":"Title","x":-296,"y":-50,"parameters":{"text":"Confirm Deletion","fontSize":{"value":30,"unit":"px"},"textAlignX":"center","marginBottom":{"value":10,"unit":"px"}},"ports":[],"children":[]},{"id":"3f2cd4e2-40c4-aa93-9296-13a66b3eff4e","type":"Text","label":"Message","x":-296,"y":10,"parameters":{"text":"Are you sure you want to delete this item?","marginBottom":{"value":20,"unit":"px"}},"ports":[],"children":[]},{"id":"78b2871e-ac6c-8ab3-9aab-2c844948bfa9","type":"Group","label":"Button Group","x":-296,"y":70,"parameters":{"flexDirection":"row"},"ports":[],"children":[{"id":"7ff68987-b14b-a7cd-41e9-22f3dc419a72","type":"net.noodl.controls.button","label":"Confirm","x":-276,"y":130,"parameters":{"label":"Confirm","backgroundColor":"#269B2B"},"ports":[],"children":[]},{"id":"35c641a6-bd3a-9801-20a3-b88b38e4f1f9","type":"net.noodl.controls.button","label":"Cancel Button","x":-276,"y":190,"parameters":{"label":"Cancel","alignX":"right","backgroundColor":"#F61934"},"ports":[],"children":[]}]}]}]}],"connections":[]}} />
</div>
## Passing data and signals to and from Popups
Great, we have our first Popup. Now we need to do something with it. We need to know whether the user clicked "Confirm" or "Cancel" and of course close our popup. You can do this using the **Close Popup** node. Add one to your Popup Component. Click on it to see its properties.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/popups/close-popup-1.png)
</div>
As you can see it has two sections: **Results** and **Close Actions**.
In the **Results** section you can add any number of inputs to pass data from the **Popup** back to wherever it was opened. And **Close Actions** is used to pass actions (i.e. signals). Whatever you add here, will be added as output signals to the **Show Popup** node that opened the **Popup**. We need to be know whether the user clicked **Confirm** or **Cancel**. The easiest way to pass this information is as signals. Create two **Close Actions** one called `User Confirmed` and one called `User Cancelled`. Connect the two signals to the respective button.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/popups/popup-visuals-6.png)
</div>
Then go back to the main App to use the signals to handle the delete. We simply hook up the **User Confirmed** signal to the **Do** signal of the **Remove Object From Array** node. Easy!
<div className="ndl-image-with-background xl">
![](/docs/guides/navigation/popups/list-4.png)
</div>
Nice. Let's also pass some data to the **Popup**, the name of the item we want to delete.
First we need to assign the **Id** we get from the **Repeater** to an **Object**. Create and **Object** node and add two properties, `firstname` and `lastname`. Connect the **Item Id** from the **Repeater** to the **Id** of the **Object**.
<div className="ndl-image-with-background xl">
![](/docs/guides/navigation/popups/list-5.png)
</div>
Then we need to go into our confirmation Popup and add in a **Component Inputs**. Add two properties, **firstname** and **lastname**. We want to incorporate the first and last name into our message. So also add a [String Format](/nodes/string-manipulation/string-format) node. Enter the following text in the node:
```
Are you sure you want to delete the item "{firstname} {lastname}"?
```
`firstname` and `lastname` will now become inputs on the **String Format** node and you can connect the outputs `firstname` and `lastname` from the **Component Inputs** node. Finally connect the **Formatted** output from the **String Format** node to the text of the "Message" **Text** node.
<div className="ndl-image-with-background xl">
![](/docs/guides/navigation/popups/popup-visuals-7.png)
</div>
The last step is to connect the `firstname` and `lastname` of the **Object** to our two new inputs on the **Show Popup** node, coming from the **Component Inputs** we just added.
<div className="ndl-image-with-background xl">
![](/docs/guides/navigation/popups/list-6.png)
</div>
## Advanced Styling
Let's make our Popup a little more dynamic by adding some animations to it. We can use the **Did Mount** signal from our root **Group** in the Popup component to trigger a transition when the Popup is opened. We can also use some fancy CSS for our backdrop. Let's start with the latter.
Click on the root node of the Popup Component. Find the attribute **CSS Style**. Set it to `backdrop-filter: blur(5px);`
<div className="ndl-image-with-background">
![](/docs/guides/navigation/popups/css-style.png)
</div>
This adds an extra blurring effect to the background. Note that this CSS-effect is not supported by old browsers.
Then we want to animate the size of the Popup when the it appears and disappears. Start by adding a [States](/nodes/utilities/logic/states) node. Give it two states: `Gone` and `Here`. Create a value called `scale`. Set it to be 0 in the `Gone` state and 1 in the `Here` state. Make sure the initial state is `Gone`.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/popups/animation-1.png)
</div>
Also set the length of each transition to 100 ms - this transition need to go fairly fast to not be annoying.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/popups/animation-2.png)
</div>
Then make the **States** node go to the `Here` state when **Did Mount** trigger. Connect the `scale` output to the **Scale** value of the **Popup** group.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/popups/animation-3.png)
</div>
Try it!
To reverse the transition when closing the Popup is a little more complicated because we need to wait for the transition to end before we send our **Close Popup** signals. So we need to add some logic.
First connect **Click** from both buttons to **To Gone** on our **States** node. That will trigger the transition. Then, we need to store which **Button** that was clicked wo we can send the correct signal to **Close Popup**. We do that using a [Switch](/nodes/logic/switch) that's set to either On or Off (true or false) depending on which button that was pressed.
When the animation is over (the **At Gone** signal will be triggered) we evaluate a [Condition Node](/nodes/utilities/logic/condition) to see which close signal we want to trigger.
<div className="ndl-image-with-background xl">
![](/docs/guides/navigation/popups/animation-4.png)
</div>
## Building a Wizard-type Popup
As you probably know an App typically contain many types of **Popups**, for example Error Messages, confirmation dialogues and maybe multistep wizards. For that reason we want to refactor our Popup and make parts of it reusable and to make all **Popups** consistent in how they look and behave.
## Making a Re-usable Popup Component
First we want to figure out what parts that all **Popups** should have. In this case we want the generic parts to be
* The background and layout of the **Popup** but not the content inside it (e.g. the text message and the **Buttons**)
* The title text
* The animation behavior
Let's start by duplicating our `Confirm Delete Popup` component.
<div className="ndl-image-with-background">
![](/docs/guides/navigation/popups/duplicate-1.png)
</div>
Rename it to `Popup Base`. The idea is that all popups should be based on this component going forward.
We want to remove everything that is specific to the previous delete confirmation popup.
Remove the **String Format** node and the properties from the **Component Inputs** (`firstname`, `lastname`). Instead add a new property called `Title`. Connect it to the **Text** property of the Title **Text** node.
We also need to remove the message, the **Buttons** and the logic connected to the **Close Popup**. Instead we are going to use a [Component Children](/nodes/component-utilities/component-children) node, that will give our component the ability to have nodes as children. The children will be inserted exactly where the **Component Children** resides in the tree.
Left to do is to handle the transition when the **Popup** is shown and closed. The showing part can be handled completely in the `Popup Base`, but the closing transition is different, since the **Close Popup** logic will be different for different Popups. The solution is to let the `Popup Base` component handle the close transition and tell when the transition is done. The user of the component also need to be able to tell the `Popup Base` when the transition should start.
So add a [Component Outputs](/nodes/component-utilities/component-outputs) node and add a property `Close Transition Done` to it. Then connect the **Has Reached Gone** signal from the **States** node to it. Add another property on the **Component Input** called **Start Close Transition** and connect it to the `To Gone` input of the **States** node.
All in all the popup base will now look like this:
<div className="ndl-image-with-background xl">
![](/docs/guides/navigation/popups/popup-base-1.png)
</div>
### Using the Popup Base in the Confirm Delete Popup
Ok, so let's refactor our `Confirm Delete` popup to use our `Popup Base` component. Open it.
The first thing we will do is of course to add in a `Popup Base` node. This will be our new root once we moved all specific nodes to it.
Start by moving the Message **Text** node and the Button Group **Group** node to become children of our `Popup Base` node. Remember, they will be inserted into the tree according to the position of our **Component Children** node.
<div className="ndl-image-with-background xl">
![](/docs/guides/navigation/popups/popup-visuals-8.png)
</div>
It's a bit of a mess now, because we have two root nodes. We will fix that soon.
Also make sure the **Title** input of our `Popup Base` node is correct by clicking on the node. It should be ok, because the default text came from this component when we did the refactoring.
Actually the only thing we need to hook up now is the logic for the closing of the **Popup**. Remove the **States** node, that one is part of the `Popup Base` component now. Reconnect the **Click** outputs of the buttons to the `Start Close Transition` signal on the `Popup Base`. Then connect the outgoing signal **Close Transition Done** to the **Evaluate** input of the **Condition** node. We can now remove the old node tree and only have the one based on the `Popup Base` node.
<div className="ndl-image-with-background xl">
![](/docs/guides/navigation/popups/popup-visuals-9.png)
</div>
Refactoring done! Look, much cleaner looking tree. Make sure the popup still looks and works as before (it should).
You can import the complete project by clicking the Import button below.
<div className="ndl-image-with-background l">
![](/docs/guides/navigation/popups/popup-final.png)
<ImportButton zip="/docs/guides/navigation/popups/popups.zip" name="Popup Example" thumb="/docs/guides/navigation/popups/popup-final.png"/>
</div>
If you want to see how to use **Popups** together with **Component Stack** you can continue building on this example in the [Component Stack Guide](/docs/guides/navigation/component-stack).

0
docs/guides/overview.md Normal file
View File

View File

@@ -0,0 +1,50 @@
---
title: Building User Interfaces Basics
hide_title: true
---
import useBaseUrl from '@docusaurus/useBaseUrl'
# Building User Interfaces Basics
## What you will learn in this guide
In this guide you will learn how to place and group components in the visual canvas.
## Adding UI elements
To add UI controls and other UI elements, such as Texts, to the currently selected visual component you click the **[+]** icon at the top of the visual canvas. This brings up the **Node Picker**, here you can find yourself to the **UI Elements** section. Click the element you want to add and it is added to the currently selected component.
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/user-interfaces/basics/add-ui-control.mp4")}/>
</div>
## Editing properties
To edit the properties of a UI element, you click the element in the visual canvas (or more commonly in the node graph editor) which will bring up the properties panel. Here you can edit all properties of the UI element.
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/user-interfaces/basics/edit-properties.mp4")}/>
</div>
## The visual heirarchy
As you add UI elements to your component you will see the visual heirarchy being built in the node graph editor. All UI elements are depicted as blue nodes. Here are some nifty things to know about the visual hierarchy in the node editor.
- All components must have a **single root** UI Element, most ofthen this is a **Group** node.
- You can hover the UI elements in the node graph editor to reveal them in the visual canvas.
- You can manipulate the heirarchy much like you would a layer panel in other design tools.
- You can bring up the **Node picker** by **right clicking** in the node graph editor. If you want the newly created UI element to be placed as a child to another UI element simply right click the parent element to bring up the **Node Picker**.
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/user-interfaces/basics/ui-heirarchy.mp4")}/>
</div>
## Grouping and Layouts
Now that you know how to place new components it's time to learn another important concept, grouping. This is how you group UI elements together and control the layout of the elements under a group. Use the **Node Picker** to create a new group, place it where you want it in the heirarchy, select, drag and drop the UI elements that you want under the group.
The **Group** node gives you a bunch of options for layouting user interfaces, learn more about the details of layouting and the group node in this [guide](/docs/guides/user-interfaces/layout)
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/user-interfaces/basics/grouping.mp4")}/>
</div>

View File

@@ -0,0 +1,93 @@
---
title: Components
hide_title: true
---
import useBaseUrl from '@docusaurus/useBaseUrl'
# Components
## What you will learn in this guide
In this guide you will learn how to create visual components to make re-usable UI components. This is essential to create dynamic user interfaces connected to data that we will learn later.
## Creating visual components
Visual components are parts of a user interface that can be reused. You create new visual components using the **Component Panel**.
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/user-interfaces/components/create-component.mp4")}/>
</div>
Next you give it a name and it will show up in the components list, as well as in the visual canvas. Newly created visual components only contain a single root **Group** node. Once you have your component created you can start working on it like you have already learnt, or you can cut and paste UI elements from another component that you want to make re-usable.
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/user-interfaces/components/cut-n-paste.mp4")}/>
</div>
When you have your component all done, you can re-use it anywhere in your application user interface by simply dragging it from the component panel.
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/user-interfaces/components/drag-component.mp4")}/>
</div>
If you prefer, you can also find your components in the node picker.
<div className="ndl-image-with-background l">
![](/docs/guides/user-interfaces/components/create-from-node-picker.png)
</div>
## Component Inputs & Outputs
That's a pretty neat trick. Now we can create our own re-usable UI components. But it would be even more powerful if we could turn them into templates and change some things up for each new instance of the UI component we create. This is where **Component Inputs** come into play. This is a concept where you can expose certain inputs of some of your nodes in the component as inputs to the component instance. This is done with the **Component Inputs** node.
<div className="ndl-image-with-background l">
![](/docs/guides/user-interfaces/components/component-inputs.png)
</div>
If you go ahead and edit the properties of this node you will see that you can add **ports** to it. Each port will become an input (and property) of your component instances when you use the component in your application. In this example, we create two **ports** one called **Label** and another called **Button Color**.
<div className="ndl-image-with-background m">
![](/docs/guides/user-interfaces/components/create-input.png)
</div>
After that is done, we can go ahead and make connections from the **Component Inputs** node to the inputs that we want each **Port** to connect to.
- So the **Label** port we want to connect to the **Text** input of the **Text** node.
- And the **Button Color** port we want to connect to the **Background Color** of the **Button** node.
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/user-interfaces/components/connect-inputs.mp4")}/>
</div>
Now with your new component inputs in place, you will see that the ports show up as properties on your component instance and you can go ahead and customize them individually.
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/user-interfaces/components/edit-inputs.mp4")}/>
</div>
This works in the same way for component outputs, you simply use the **Component Outputs** node instead. This is very useful when you are creating UI component that accept some sort of user intput via UI controls.
<div className="ndl-image-with-background l">
![](/docs/guides/user-interfaces/components/component-outputs.png)
</div>
Component inputs and outputs, and connecting to these is not just important for making re-usable UI components. It's also a core concept in connecting data to your user interfaces and creating dynamic data driver applications. We will take a closer look at that in the [Working with data](/docs/guides/data/overview) section.
## Sheets
When your application grows you will be creating more and more components and after a while the visual canvas can become crowded. When this happends it's a good idea to orginise your application into sheets. Each sheet is a new empty visual canvas for you to place more components into. To create a new sheet
<div className="ndl-image-with-background m">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/user-interfaces/components/create-sheet.mp4")}/>
</div>
You can move components between sheets by simply dragging from the components panel and dropping onto the sheet you want to move it to.

View File

@@ -0,0 +1,83 @@
---
title: Figma Plugin
hide_title: true
---
# Figma plugin
:::info
Download the plugin here: https://www.figma.com/community/plugin/1006908263044642341/Noodl
:::
If you're a Figma user you can use Figma documents to create nodes in Noodl.
Layers in Figma will be mapped to nodes in Noodl, and complex shapes will be exported as images.
The Noodl plugin can export:
- Shapes like circles, rectangles and lines with one fill or stroke. These will be mapped to the corresponding Noodl nodes.
- Complex shapes, like vectors paths and boolean operation, will be exported as images
- Text styles
- Positions and sizes will map to absolute layouts and fixed dimensions
- Entire layer hierarchies, with the correct order and parent/child relations.
The Noodl plugin can't currently export:
- Auto layout
- Constraints
- Prototype interactions
This guide uses the Mobile UI kit by Toni Gemayel — a design resource from the Figma community:
https://www.figma.com/community/file/836596421863073964/Mobile-UI-kit
## Export a layer
1. Select a layer
2. Open the Noodl plugin and click "Export". Make sure Noodl is running and have a project open.
3. Noodl will now become focused. Place the Noodl node in your node hierarchy. Assets, like images, will be automatically placed in your project directory.
<iframe width="560" height="315" style={{margin:'0 auto',display:'block'}} src="https://www.youtube-nocookie.com/embed/GsczhwfoyEE" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe><br/>
Note that the Ellipse in Figma was mapped to an `Image` node in Noodl, with the correct size, image asset and border.
:::info
Here's the same node duplicated in Noodl, with a different source and border radius
:::
![](/docs/guides/user-interfaces/figma-plugin/image2.png ':class=img-size-l')
## Export text and text styles
All text styles used by exported text layers will automatically be imported.
?> Figma uses the fonts you have installed on your system. In Noodl apps the required font files have to be defined in your projects. You can [import fonts via Google Fonts](https://www.youtube.com/watch?v=lgMZZC6XoAs) or [via font files](https://www.youtube.com/watch?v=P76v0Q38eKI)
<iframe width="560" height="315" style={{margin:'0 auto',display:'block'}} src="https://www.youtube-nocookie.com/embed/sZm0eBZvLaM" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe><br/>
?> Note that the text styles was added to the styles tab in Noodl ![](/docs/guides/user-interfaces/figma-plugin/text-styles.png ':class=img-size-l')
## Export complex shapes
Complex shapes that don't map to a visual node in Noodl, like this vector path, will be exported as images. You can use the "Default image size" option to specify what resolutions the images should be exported at.
<iframe width="560" height="315" style={{margin:'0 auto',display:'block'}} src="https://www.youtube-nocookie.com/embed/mqML1OL0SUk" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe><br/>
## Export larger layer structures
To get started quickly you can export entire structures, like a whole page. As you progress building the application and you can extract components and replace the static layouts with dynamic layouts and make the design data-driven.
<iframe width="560" height="315" style={{margin:'0 auto',display:'block'}} src="https://www.youtube-nocookie.com/embed/5miB0PD4z9k" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe><br/>
:::info
Another page exported all at once
:::
![](/docs/guides/user-interfaces/figma-plugin/whole-page.png ':class=img-size-l')
## Notes about workflow
Most application are more dynamic than what's typically designed in Figma. Text inputs are interactive, labels and values are data-driven, the amount of items in a list isn't fixed, groups can have padding, and so on. This means that there's some work left to do on the nodes that are exported.
- **Applications**: Export small parts, like a list item, a button, and build them up one by one in Noodl. Entire structures are harder to export as they often need to be restructured.
- **Prototypes**: Export entire pages, one by one, and build up the navigation structure in Noodl. Then start replacing the parts that require dynamic and interactive elements, and adjust the layout where it needs to be more flexible.

View File

@@ -0,0 +1,555 @@
---
title: Grouping & Layout
hide_title: true
---
import useBaseUrl from '@docusaurus/useBaseUrl'
import CopyToClipboardButton from '../../../src/components/copytoclipboardbutton';
# Grouping & Layout
## What you will learn in this guide
This guide will cover how to lay out visual elements. It's an important concept for creating responsive, dynamic interfaces for your apps.
The central node for defining a layout in Noodl is the [Group](/nodes/basic-elements/group/) node. This guide will walk through the most important Group properties for defining a layout.
### Overview
We will cover the following topics in this guide:
- The Group Node
- Layout Direction and position
- Size and Dimensions
- Alignment
- Margin and Padding
- Multi Line Layout
## The Group Node
The **Group** node is the most central node for doing a layout. It's the fundamental way of building visual hierarchies and structures in Noodl.
**Group** nodes are arranged in a hierarchy and can have other nodes as children. It controls the layout of its children and there are a number of properties that can be used to specify how the children of a **Group** node will be laid out. You can copy the various node examples used in this guide by clicking "Copy nodes" and then pasting into a Noodl project.
<div className="ndl-image-with-background xl">
<img src="/2.8/docs/guides/user-interfaces/layout/groups.png" />
<CopyToClipboardButton
json={{
nodes: [
{
id: '5a81ad6d-b8eb-16f8-e2b3-3533c0a05462',
type: 'Group',
label: 'Group',
x: 176,
y: 140.5,
parameters: { backgroundColor: '#FFFFFF' },
ports: [],
children: [
{
id: '999f7f8b-6678-ed24-5ea9-08b25e1b394e',
type: 'Group',
x: 196,
y: 186.5,
parameters: {
marginTop: { value: 20, unit: 'px' },
marginLeft: { value: 20, unit: 'px' },
marginRight: { value: 20, unit: 'px' },
marginBottom: { value: 20, unit: 'px' },
paddingTop: { value: 50, unit: 'px' },
paddingLeft: { value: 50, unit: 'px' },
paddingRight: { value: 50, unit: 'px' },
paddingBottom: { value: 50, unit: 'px' },
backgroundColor: '#DBDBDB',
},
ports: [],
children: [
{
id: 'ddde9610-f9a6-03bd-bb46-4a67d56a2180',
type: 'Group',
x: 216,
y: 232.5,
parameters: { backgroundColor: '#B5B5B5' },
ports: [],
children: [],
},
],
},
],
},
],
connections: [],
}}
/>
</div>
:::tip
Hovering on a node in Noodl with the mouse cursor will highlight the corresponding visual element in the preview window. This is a great way to debug your layout. For example you hover over a node and you cannot see it in the preview window, it's likely that it has a zero size or lies outside the screen.
:::
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/user-interfaces/layout/hover.mp4")}/>
</div>
## Layout direction
By default the **Group** node will stack its children vertically. You can change the layout direction by editing the **Layout** property:
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/layout/layout-prop.png)
</div>
- `Vertical` Children are stacked vertically.
- `Horizontal` Children are stacked horizontally.
- `None` Children are _not_ stacked. You will have to position them yourselves, for example by using margins, x/y positions or various alginments (see below).
Here's an example you can copy into Noodl. Click the "Copy nodes" button next to the image and press Ctrl+V (Windows) or Cmd+V (macOS) to paste them into Noodl. Make sure you have a project open.
Change the **Layout** of the top level node to see how the direction of the child nodes change.
<div className="ndl-image-with-background xl">
<img src="/2.8/docs/guides/user-interfaces/layout/layout-dir.png"></img>
<CopyToClipboardButton
json={{
nodes: [
{
id: '9f88e7d6-959c-7946-f66d-3db6257d522b',
type: 'Group',
label: 'Layout node',
x: 290,
y: 395,
parameters: {
sizeMode: 'explicit',
backgroundColor: '#E6E6E6',
},
ports: [],
children: [
{
id: '8a0b2c8c-c534-bdcc-102d-2905b48d8885',
type: 'Group',
x: 310,
y: 456,
parameters: {
marginTop: { value: 5, unit: 'px' },
marginLeft: { value: 5, unit: 'px' },
marginRight: { value: 5, unit: 'px' },
marginBottom: { value: 5, unit: 'px' },
backgroundColor: '#C2C2C2',
width: { value: 100, unit: 'px' },
height: { value: 100, unit: 'px' },
},
ports: [],
children: [],
},
{
id: 'bcfa83ad-f05f-d7ff-13a4-f82ac19bd664',
type: 'Group',
x: 310,
y: 502,
parameters: {
marginTop: { value: 5, unit: 'px' },
marginLeft: { value: 5, unit: 'px' },
marginRight: { value: 5, unit: 'px' },
marginBottom: { value: 5, unit: 'px' },
backgroundColor: '#C2C2C2',
width: { value: 100, unit: 'px' },
height: { value: 100, unit: 'px' },
},
ports: [],
children: [],
},
{
id: '35c2039f-5f12-3dab-bc94-a7c630aa9698',
type: 'Group',
x: 310,
y: 548,
parameters: {
marginTop: { value: 5, unit: 'px' },
marginLeft: { value: 5, unit: 'px' },
marginRight: { value: 5, unit: 'px' },
marginBottom: { value: 5, unit: 'px' },
backgroundColor: '#C2C2C2',
width: { value: 100, unit: 'px' },
height: { value: 100, unit: 'px' },
},
ports: [],
children: [],
},
],
},
],
connections: [],
}}
/>
</div>
## Layout Position
As a child of a group you can also control how you will be layouted by setting the **Position** property:
- `In Layout` - This node is part of the parent node's layout. It will be stacked with its siblings depending on the parent node's layout settings as explained above.
- `Absolute` - Removes a node from the flow of a layout. Use margins and alignment to set the position.
- `Sticky` - Behaves like `In Layout`, except when the node is about the be scrolled outside the parent. It'll stick to an edge of the parent instead of scrolling away. Which edge can be controlled with the _Alignment_ input.
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/layout/layout-absolute.png)
</div>
## Dimensions
The dimensions section control how the size of a **Group** will be calculated.
<div className="ndl-image-with-background">
![](/nodes/ui-elements/dimensions.png)
</div>
A **Group** can also get its dimensions from the size of its children. You use the icons at the top to change between the four modes (from right):
- **Explicit width & height** Specify both width and height explicitly in pixels (`px`), percentage of parent (`%`), or as a percentage of the viewport size (`vw` and `vh`).
- **Explicit width & content height** Specify the width explicitly but the height will be the total of the children heights and margins.
- **Explicit height & content width** Specify the height explicitly but the width will be the total of the children widths and margins.
- **Content width & height** The size of this group will be the total width and height of the children based on the layout.
You can copy the nodes below to an empty component in Noodl. The result is shown to the right. These nodes each use a different setting for the **Dimensions** icons.
<div className="ndl-image-with-background">
<img
src="/2.8/docs/guides/user-interfaces/layout/content-size-nodes.png"
className="ndl-image small"
></img>
<img
src="/2.8/docs/guides/user-interfaces/layout/content-size-example.png"
className="ndl-image small"
></img>
<CopyToClipboardButton
json={{
nodes: [
{
id: '3aad82a0-9636-d391-b31c-06f66be77bdc',
type: 'Group',
x: 243,
y: 178,
parameters: { backgroundColor: '#FFFFFF' },
ports: [],
children: [
{
id: '61757927-9c6c-3afb-a964-1492a5ff5a90',
type: 'Group',
label: 'Content height',
x: 263,
y: 224,
parameters: {
sizeMode: 'contentHeight',
backgroundColor: '#D6D6D6',
marginBottom: { value: 20, unit: 'px' },
width: { value: 100, unit: 'px' },
},
ports: [],
children: [
{
id: 'cefc770e-47c0-ddfd-0caa-b88831ce8e8f',
type: 'Circle',
x: 283,
y: 285,
parameters: {
fillColor: '#858585',
size: 50,
},
ports: [],
children: [],
},
],
},
{
id: 'ccdbfc6b-8138-cc99-6ffc-aaf6b5c4b56e',
type: 'Group',
label: 'Content size',
x: 263,
y: 331,
parameters: {
sizeMode: 'contentSize',
backgroundColor: '#D6D6D6',
marginBottom: { value: 20, unit: 'px' },
},
ports: [],
children: [
{
id: 'cef82662-86c6-43d7-5221-fa47869a5043',
type: 'Circle',
x: 283,
y: 392,
parameters: {
fillColor: '#858585',
size: 50,
},
ports: [],
children: [],
},
],
},
{
id: '000843d3-fdf9-7638-67e3-42617b66bf4f',
type: 'Group',
label: 'Content width',
x: 263,
y: 438,
parameters: {
sizeMode: 'contentWidth',
backgroundColor: '#D6D6D6',
height: { value: 100, unit: 'px' },
},
ports: [],
children: [
{
id: 'f94a589b-b042-9f9e-1d2f-0599a5fe2f41',
type: 'Circle',
x: 283,
y: 499,
parameters: {
fillColor: '#858585',
size: 50,
},
ports: [],
children: [],
},
],
},
],
},
],
connections: [],
}}
/>
</div>
### Fixed dimension
Sizes specified in `%` enables an additional set of dynamic size rules.
- The group will expand to fill the parent. Space is shared with siblings and the `%` value controls how much of the remaining space each node will get.
- If the content of a Group is larger than the Group itself, it'll expand to contain the children.
This dynamic sizing can be disabled by enabling _Fixed_.
Here's an example with two Groups, both set to 100% height. With _Fixed_ disabled they will both try to grow to the same size as the parent. As the rules above implied, the Groups final size will be 50% to make sure that both Groups have equal space and are shown on the screen.
Enabling _Fixed_ will force a Group to be exactly the size that's specified and disable dynamic sizing. With _Fixed_ enabled the first Group will cover the entire screen, and there will be no space left for the second Group that'll be pushed outside the screen.
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/layout/fixed-height.gif)
</div>
### Clip content
A Group with dimensions set in `%` will expand to make sure it at least the same size as all of its content.
This can be changed be either enabling `Fixed` or as in this example, enable `Clip content`.
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/layout/clip.gif)
</div>
## Alignment
You can use the alignment controls to pin the children to a specific edge, or how the remaining space should be distributed among the children. The padding of a group determines where that edge is.
A Group can control the alignment of its children using the _Align and justify content_ property.
<div className="ndl-image-with-background l">
<img src="/2.8/docs/guides/user-interfaces/layout/alignment.gif"></img>
<CopyToClipboardButton
json={{
nodes: [
{
id: '6bede195-ea0c-d5ca-a315-500079267560',
type: 'Group',
x: 198.0,
y: 196,
parameters: { backgroundColor: '#D6D6D6' },
children: [
{
id: 'b5beb893-c7ae-9467-da03-0032b8221ab6',
type: 'Circle',
x: 218.0,
y: 242,
parameters: { fillColor: '#858585' },
},
{
id: '0404c68a-525c-f975-60d3-8d0f7143abb7',
type: 'Circle',
x: 218.0,
y: 288,
parameters: { fillColor: '#858585' },
},
{
id: 'b2b85213-f75e-7d0b-050e-dbee3c312fd7',
type: 'Circle',
x: 218.0,
y: 334,
parameters: { fillColor: '#858585' },
},
],
},
],
}}
/>
</div>
Children can use the _alignment_ controls to override the parent's alignment settings.
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/layout/align-props.png)
</div>
Children can use margins to offset the position from how it's aligned.
<div className="ndl-image-with-background l">
<img src="/2.8/docs/guides/user-interfaces/layout/align.gif"></img>
<CopyToClipboardButton
json={{
nodes: [
{
id: '8a968ff3-9099-fc23-eaef-b3f3c2f8a271',
type: 'Group',
label: 'Layout None',
x: -124.8300537163023,
y: 231.19983547495616,
parameters: {
backgroundColor: '#FFFFFF',
flexDirection: 'none',
},
ports: [],
children: [
{
id: '6c2df5ce-7596-06da-a5c5-335275501d64',
type: 'Circle',
x: -104.8300537163023,
y: 292.19983547495616,
parameters: {
fillColor: '#C9C9C9',
alignY: 'top',
alignX: 'left',
},
ports: [],
children: [],
},
],
},
],
connections: [],
}}
/>
</div>
## Margin and padding
Clicking on a node will select it and open the property panel. This panel has controls for setting the margin and padding.
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/layout/margin-and-padding-props.png)
</div>
Here you can specify the _margin_, i.e. the distance between this node and its siblings in the layout. You can specify the margins in all four directions. You can also specify the _padding_, which is the distance from the borders of the **Group** to its children.
A good way to learn these concepts is to adjust and tweak the three nodes in this simple hierarchy. Try modifying the padding and margins and note the difference between them.
## Multiple lines
If the child nodes extend beyond the border of the parent you have a couple of options to control the behavior, wrapping or scrolling. This section will explain the wrapping options that are available.
Wrapping is controlled by the **Multi Line Wrap** option in the properties.
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/layout/wrap.png)
</div>
You can choose any of these options:
- `Off` Children will extend beyond the borders of the parent node (default).
- `On` Children will be placed on multiple lines
- `On Reverse` The children will wrap but in the reverse direction.
Here's an example of a horizontal layout with **Multi Line Wrap** set to `On`.
<div className="ndl-image-with-background l">
<img src="/2.8/docs/guides/user-interfaces/layout/multi-line.gif"></img>
<CopyToClipboardButton
json={{
nodes: [
{
id: '4ea33114-1bd2-d35a-79b6-9c6daf4ba473',
type: 'Group',
x: 198.0,
y: 196.0,
parameters: {
backgroundColor: '#D6D6D6',
flexDirection: 'row',
flexWrap: 'wrap',
},
children: [
{
id: 'beeb24c5-2bf0-ca33-62ce-b448a10f1b09',
type: 'Circle',
x: 218.0,
y: 242.0,
parameters: { fillColor: '#858585' },
},
{
id: '1742b597-6ee5-dd00-0781-ec3c368b9bdc',
type: 'Circle',
x: 218.0,
y: 288.0,
parameters: { fillColor: '#858585' },
},
{
id: 'e4fc4976-3c13-4a5d-701c-ef3a72eb17b5',
type: 'Circle',
x: 218.0,
y: 334.0,
parameters: { fillColor: '#858585' },
},
{
id: '37441ff4-81ac-47ac-2882-8d3f5a4151b4',
type: 'Circle',
x: 251.53808416795107,
y: 210.45256017010956,
parameters: { fillColor: '#858585' },
},
{
id: '4d7a3378-7b80-1f6d-c0cd-285360f291f6',
type: 'Circle',
x: 251.53808416795107,
y: 256.45256017010956,
parameters: { fillColor: '#858585' },
},
{
id: 'b1a7b659-7519-e8ea-f4f5-96bba6954b81',
type: 'Circle',
x: 251.53808416795107,
y: 302.45256017010956,
parameters: { fillColor: '#858585' },
},
],
},
],
}}
/>
</div>

View File

@@ -0,0 +1,56 @@
---
title: Modules and Prefabs
hide_title: true
---
# Modules and Prefabs in Noodl
Noodl has a lot of nodes available out of the box, but it can never include a node for everything. For that reason you can extend your project with specific modules or prefabs containing nodes and components to capture specfic functionality or UI controls.
The main difference between **Modules** and **Prefabs** is that **Modules** install new nodes to the editor, while **Prefabs** clones components built with the core nodes, and stores them as if you created them yourself. Prefabs can be imported as many times as you want, and each clone can be modified how you see fit.
You can see the list of modules [here](/library/modules/overview), and prefabs [here](/library/prefabs/overview). Make sure to check in often as the library keeps growing quickly.
## How to add modules and prefabs to your project
You add modules to your project by opening the project you want to use the module in and the bring up the **Node Picker**. You can do this either by **Right Clicking** in the node canvas or by clicking the `+` icon at the top left.
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/modules/add-new-node.png)
</div>
In the node picker, the find the tab **Modules** and then click **Import** on the module you want to use. The module is added to your project.
<div className="ndl-image-with-background l">
![](/docs/guides/user-interfaces/modules/browse-modules.png)
</div>
After importing the module you new nodes and components are now available under **External Libraries**.
<div className="ndl-image-with-background l">
![](/docs/guides/user-interfaces/modules/external-libraries.png)
</div>
Adding a prefab is done via the **Prefabs** tab in the same way, click **Import**. Prefabs will add a component to your library of components. If you already have components with that name (maybe you have included the prefab before), you will be asked to overwrite.
<div className="ndl-image-with-background l">
![](/docs/guides/user-interfaces/modules/browse-prefabs.png)
</div>
When the prefab is imported you will get one or more new components in your **default sheet**.
<div className="ndl-image-with-background l">
![](/docs/guides/user-interfaces/modules/prefabs-imported.png)
</div>
Don't forget to read the documentation for the prefabs you intend to use, there you will find how to use them but also useful guides on how to customize and build upon them to your liking.

View File

@@ -0,0 +1,18 @@
---
title: Building User Interfaces
hide_title: true
---
# Building User Interfaces in Noodl
The user interface in your Noodl App are all the things that your users will interact with on your screen. Lists, buttons, checkboxes, etc.
In Noodl, you build these using **Visual Nodes**, i.e. the blue nodes in your node graph. Visual nodes represents something that can be seen on a screen. In contrast, logical nodes, for example representing a query to the database or a condition to be evaluated are not directly visible on the screen.
The visual nodes can also be grouped together in hierarchies mainly to define how they are relate to each other in the layout on the screen. By putting two **Buttons** as children of the same **Group** you can for example hide both these **Buttons** by hiding the **Group**. The order of the visual nodes also generally tells which order they will be rendered on the screen. Nodes later in the tree will be rendered on top of nodes earlier in the tree, if they happen to occupy the same space in the tree. (However this can be overridden using the **zIndex** property.)
As you can understand, for a large App, the visual tree can quickly become very large. That's why you typically create **Components** of subtrees that capture a specific functionality. The components can in themselves contain more components. Components are also a great way to re-use UI in many places in your app and create a design system greatly reducing time to build UI.
The visual nodes can also be styled and customized heavily from a visual perspective. Also the visualization aspect of a visual node can be saved and re-used using the style variants system. Many visual nodes also have multiple visual states. For example a Button will typically change appearance when the user hovers over it, clicks it or if the button is disabled. Using the visual states system or the **States** node, you can finetune in detail how you UI will react visibly to ser interactions.
All of this and more is covered in this part of the guide.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,270 @@
---
title: Scrolling Content
hide_title: true
---
import ImportButton from '../../../src/components/importbutton'
import ReactPlayer from 'react-player'
# Scrolling Content
## What you will learn in this guide
This guide will teach you the basics on how to make content larger than its container visible through scrolling. We will mainly make use of the **Scrolling** property on the [Group](/nodes/basic-elements/group) node and go into detail on how the layout engine works in relation to scrolling. We will also look at the built in scrolling functionality in the [Page Router](/nodes/navigation/page-router) that makes it easy to build pages with scrollable content.
## Overview
The guides will walk through the following topics
- How the **Group** node gets it size
- Scrolling in the **Group** node
- Scrolling in the **Page Router** node.
- `Sticky` Layout Position in a scrolling **Group**
This guide will use the [Repeater](/nodes/ui-controls/repeater) node to create lists and also touch on navigation using the [Page Router](/nodes/navigation/page-router). So it's recommended to check out the guides for them either before or after following this guide.
- [List Basics Guide](/docs/guides/data/list-basics)
- [Web Navigation Guide](/docs/guides/navigation/basic-navigation)
You should also be familiar with layout basics so check out [this](/docs/guides/user-interfaces/layout) guide before you start this guide, if you haven't already.
## Scrolling in the Group node
The **Group** node is the fundamental node to create a layout. So what happens if the children of a **Group** node takes up more space than what's available?
Well, it depends on how you set up its sizing.
### The size of the Group node
On a high level there are two options (in both horizontal and vertical direction):
1. Size is explicitly set. The size can then be given in % of available space, pixels or vw
2. Size is decided by the total size of its children
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/scrolling-content/dims-1.png)
</div>
So we only need to worry about case 1), what happens if the children takes up more than the space that you give them in the Group?
Let's try it. Start a new project, for example using the "Hello World" template. Replace the current nodes with the ones below.
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/scrolling-content/step-1.png)
</div>
Fill up the **Static Array** node with some data. Make sure its set to `JSON` format.
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/scrolling-content/static-array-1.png)
</div>
Then copy and paste the data below into the **Static Array**.
```json
[
{ "label": "item 1" },
{ "label": "item 2" },
{ "label": "item 3" },
{ "label": "item 4" },
{ "label": "item 5" },
{ "label": "item 6" },
{ "label": "item 7" },
{ "label": "item 8" },
{ "label": "item 9" },
{ "label": "item 10" },
{ "label": "item 11" },
{ "label": "item 12" },
{ "label": "item 13" },
{ "label": "item 14" },
{ "label": "item 15" },
{ "label": "item 16" },
{ "label": "item 17" },
{ "label": "item 18" },
{ "label": "item 19" },
{ "label": "item 20" }
]
```
Then create a new visual component and call it "List Item".
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/scrolling-content/new-component.png)
</div>
And create the following node structure:
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/scrolling-content/list-item-1.png)
</div>
Make sure the **Object** node has a property called `label` connected to the **Text** node. Also set the size of the **Group** node to 100% width, and 45 px height. Give it a 5 px margin at the bottom. Also give it a outline so we can see it properly.
Center align the **Text** node vertically.
Finally go back to the "App" component and select the newly created list item as the **Template**.
You will now have something that looks like below. (You can also import the project directly by clicking "Import" below).
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/scrolling-content/result-1.png)
<ImportButton zip="/docs/guides/user-interfaces/scrolling-content/step-1.zip" name="Scrolling Part 1" thumb="/docs/guides/user-interfaces/scrolling-content/result-1.png" />
</div>
As you can see, the list takes up the full screen and unless you have a veeeeery long screen the items will flow outside the screen. You are not able to scroll the content.
Also try to change the vertical size of the **Group** node in the main App to `50%`.
<div className="ndl-image-with-background l">
![](/docs/guides/user-interfaces/scrolling-content/50-percent.png)
</div>
You might expect that only half the screen would be covered by the list, but you will see no change. That's because the default behavior of a **Group** with explicit size set is that _if the size of the children are larger than the size of the Group, the size will grow to fit the children_.
You can change that behavior and tell the **Group** to clip the content instead of growing. Check the **Clip Content** property of the group.
<div className="ndl-image-with-background l">
![](/docs/guides/user-interfaces/scrolling-content/clip.png)
</div>
### Getting the Group to Scroll
Another way to do it is to tell the **Group** that it allows scrolling. Uncheck the **Clip Content** and instead check **Enable Scroll**. You will now have a scrollable list.
<ReactPlayer playing autoplay muted loop url="scrolling-content/scroll1.mp4" />
As you can see there is an extra option **Use native scroll**. Generally you should have this checked, unless you are doing a very custom scroll interaction. We will not cover that in this guide.
### Structuring Scrolling Content
Change back the size to `100%` on the **Group** node.
Now let's add a title and and a footer. We can't add it to our **Group** node, because then they will scroll away with the list. So we have to restructure our layout a little.
Create a new **Group** node. Make the existing **Group** node a child of the new **Group** node.
<ReactPlayer playing autoplay muted loop url="scrolling-content/regroup.mp4" />
Now we can add a header and a footer to the highest level **Group** and they will not be affected by the scrolling.
The header and footer could for example be a **Group** node with content height and 100% width. Then add a **Text** with a larger font. Perhaps add a bit of padding in the **Group** containing the text.
<div className="ndl-image-with-background l">
![](/docs/guides/user-interfaces/scrolling-content/header-footer.png)
</div>
Now you can see that the footer and header are allowed to take its space while the scrolling content takes whatever is left and lets its content scroll if it doesn't fit.
<ReactPlayer
playing
autoplay
muted
loop
url="scrolling-content/hf-scroll.mp4"
/>
## Scrolling in Page Routers
The **Page Router** is one of the main nodes to implement navigation in Noodl. In short, the **Page Router** contains the [Pages](/nodes/navigation/page/) you navigate to. In that sense, the **Page Router** is quite similar to a **Group** node. That's why a **Page Router** also have the options to make it's content scrollable by chosing **Clip Behavior** and setting **Scrolling**.
<div className="ndl-image-with-background m">
![](/docs/guides/user-interfaces/scrolling-content/page-router-props.png)
</div>
Lets try it out!
Create a new visual component. Call it "App 2". Select "Make Home" to make it the new home component (i.e it will be shown in the viewer).
<div className="ndl-image-with-background m">
![](/docs/guides/user-interfaces/scrolling-content/make-home.png)
</div>
Add a **Page Router** as the first child of the root **Group**
<div className="ndl-image-with-background m">
![](/docs/guides/user-interfaces/scrolling-content/page-router-1.png)
</div>
Click on the **Page Router** and click `Add new Page` and add a new **Page** called `Page 1`.
We can now simplify the previous structure. Since the **Page Router** will work as the **Group** we can move the **Repeater** (and the **Static Array** that provides the items) to right under the **Page** node in our newly created **Page** component.
<div className="ndl-image-with-background m">
![](/docs/guides/user-interfaces/scrolling-content/page-1.png)
</div>
Make sure to set the **Clip Behavior** to **Scrolling** on the **Page Router** and you now have scrolling content in your **Page**. Try it out.
### Sticky Layout Position option
There is another way to get things like headers and footers to not be scrolled out of the **Group** node even if scrolling is enabled, and we will try it out on our **Page**. Instead of creating a new structure we can add our header and footer to be direct children to the **Page** node. So copy and paste them so they end up before and after our **Repeater**.
<div className="ndl-image-with-background m">
![](/docs/guides/user-interfaces/scrolling-content/page-2.png)
</div>
Also make sure the header is aligned to the top and the footer aligned to the bottom.
If you try scrolling now, they will be scrolled in and out of the screen. However if you change the **Position** setting for the header and footer **Group** to `Sticky` you will see that they will stay on screen.
<div className="ndl-image-with-background m">
![](/docs/guides/user-interfaces/scrolling-content/sticky.png)
</div>
The only issue is that you can see the list scroll behind the header and footer. This is probably not what you want.
<ReactPlayer playing autoplay muted loop url="scrolling-content/sticky1.mp4" />
You fix that by setting the background color to non-transparent white. Also since Noodl by default renders everything in the order as it's in the tree (nodes later in the tree will be rendered on top of nodes earlier in the tree if they overlap), we also need to change the **zIndex** of the header to `1` to stay on top of the **Repeater** that's below it in the tree.
<div className="ndl-image-with-background m">
![](/docs/guides/user-interfaces/scrolling-content/zindex-1.png)
</div>
You now have a working scrolling list with a sticky header and footer.
<ReactPlayer playing autoplay muted loop url="scrolling-content/sticky2.mp4" />
If you want to try out the full project, import the project below.
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/scrolling-content/result-2.png)
<ImportButton zip="/docs/guides/user-interfaces/scrolling-content/step-2.zip" name="Scrolling Part 2" thumb="/docs/guides/user-interfaces/scrolling-content/result-2.png" />
</div>

View File

@@ -0,0 +1,525 @@
---
title: States Node
hide_title: true
---
import useBaseUrl from '@docusaurus/useBaseUrl'
import CopyToClipboardButton from '../../../src/components/copytoclipboardbutton';
# States Node
A common concept in Noodl is the use of different visual states. In this guide we will cover states with an example of how to create a simple reusable switch component, and an example of hovering effects.
<div className="ndl-image-with-background s">
![](/docs/guides/user-interfaces/states/switch.gif)
</div>
The examples will cover how to use a **States** node for handling transitions and interactions.
## The visuals
First let's start with the basic visual nodes that make up the switch. This is simply a **Group** and a **Circle** with proper styling. You can copy the nodes below and paste into your own project.
<div className="ndl-image-with-background xl">
<img
src="/2.8/docs/guides/user-interfaces/states/switch-nodes.png"
className="ndl-image med"
></img>
<CopyToClipboardButton
json={{
nodes: [
{
id: 'beeb6388-461b-f1fe-d64e-be798e4b1b4d',
type: 'Group',
x: 441.71346128847347,
y: 300.98375737190554,
parameters: { backgroundColor: '#FFFFFF' },
ports: [],
children: [
{
id: '9677d929-9a49-7fc5-ae14-27a1b48e883f',
type: 'Group',
x: 461.71346128847347,
y: 346.98375737190554,
parameters: {
width: { value: 80, unit: 'px' },
height: { value: 40, unit: 'px' },
backgroundColor: '#F0F0F0',
borderRadius: 20,
alignX: 'center',
marginTop: { value: 20, unit: 'px' },
},
ports: [],
children: [
{
id: 'aeda33bf-10cb-1e76-07b2-4a368140ca65',
type: 'Circle',
x: 481.71346128847347,
y: 392.98375737190554,
parameters: {
fillColor: '#E8E8E8',
size: 40,
strokeWidth: 2,
strokeColor: '#454545',
strokeEnabled: true,
},
ports: [],
children: [],
},
],
},
],
},
],
connections: [],
}}
/>
</div>
Below you can see the styling of the two nodes. The first image shows the Circle node's properties and the second image shows the Group node's properties. Don't forget that you can hover the nodes in the graph to see their placement in the preview.
<div className="ndl-image-with-background l">
<img
src="/2.8/docs/guides/user-interfaces/states/style-props1.png"
className="ndl-image small"
></img>
<img
src="/2.8/docs/guides/user-interfaces/states/style-props2.png"
className="ndl-image small"
></img>
</div>
## The states node
A very common pattern is to represent states visually. In this example we want our switch to have two states **On** and **Off**, and we want it to have different visuals for these states. This is achieved with the **States** node. Start by creating a new [States](/nodes/utilities/logic/states) node.
Next, create the two states.
<div className="ndl-image-with-background l">
<img
src="/2.8/docs/guides/user-interfaces/states/create-states.gif"
className="ndl-image med"
></img>
</div>
The **States** node will be in one of the states that we just defined. It starts in the state specified by the **State** property that shows up when you add states. Later we will switch state by connecting a signal.
We will also specify **Values** for each of our states. In the same way you added states, add a single value and call it **Knob X**. It doesn't really matter what you call it, as long as you know what it is for. In this case it's for moving the knob's (the Circle node) X position to its correct position for the two states.
<div className="ndl-image-with-background">
<img
src="/2.8/docs/guides/user-interfaces/states/values-1.png"
className="ndl-image med"
></img>
</div>
Specify the value for each state. The X position should be **40** when the switch is in the **On** state and **0** when the switch is in the **Off** state. Enter these values in the **States** node properties.
<div className="ndl-image-with-background">
<img
src="/2.8/docs/guides/user-interfaces/states/on-values.png"
className="ndl-image med"
></img>
<img
src="/2.8/docs/guides/user-interfaces/states/off-values.png"
className="ndl-image med"
></img>
</div>
Connect the **Knob X** output of the **States** node to the **Pos X** input of the **Circle** node.
<div className="ndl-image-with-background xl">
<img src="/2.8/docs/guides/user-interfaces/states/knob-x-connected.png"></img>
</div>
As you can see the **States** node will get an output corresponding to each value that is defined under **Values**. This output will transition to the specified values when the **States** node changes state. One way to see the different states and transitions is to play with the **State** dropdown menu in the properties panel.
<div className="ndl-image-with-background xl">
<video width="100%" autoPlay muted loop src={useBaseUrl("/docs/guides/user-interfaces/states/change-state.mp4")}/>
</div>
If you hover over the output connection of the **States** node a small popup will show the current value that is being outputted. If you click this small popup, you pin the popup so that it stays visible. Now you can see the output value change as it transitions from one state to another.
You can connect the outputs of the **States** node to anything you like. In this example we connected the output to an input where they were both of the type **Number**. You can use different types than numbers. Add a new **Value** to the **States** node. Call it "Background Color" and give it a **Color** type instead of the default **Number** type. Then choose two different colors for the two states and connect the output to the **Background Color** of the **Group** node containing the **Circle**.
<div className="ndl-image-with-background">
<img
src="/2.8/docs/guides/user-interfaces/states/color-type.png"
className="ndl-image med"
></img>
</div>
Finally, we can make the **States** node toggle state when the switch **Group** node is clicked. This is achieved by connecting the **Click** signal from the **Group** to the **Toggle** input of the **States** node. This will make the **States** node jump to the next state in the list and when the last one is reached it will jump to the first one again. In this case we only have two states, so it will toggle between **On** and **Off**.
<div className="ndl-image-with-background xl">
<img src="/2.8/docs/guides/user-interfaces/states/click-toggle.png"></img>
<CopyToClipboardButton
json={{
nodes: [
{
id: 'f0d8169f-90ca-9cd9-bc88-af4c2ed631d0',
type: 'Group',
x: 441.71346128847347,
y: 300.98375737190554,
parameters: { backgroundColor: '#FFFFFF' },
ports: [],
children: [
{
id: 'f2461a9d-49c6-f184-1ac1-a75450a7c856',
type: 'Group',
x: 461.71346128847347,
y: 346.98375737190554,
parameters: {
width: { value: 80, unit: 'px' },
height: { value: 40, unit: 'px' },
backgroundColor: '#F0F0F0',
borderRadius: 20,
alignX: 'center',
marginTop: { value: 20, unit: 'px' },
},
ports: [],
children: [
{
id: 'c66997cc-19d2-630c-692e-0caafdf37dd7',
type: 'Circle',
x: 481.71346128847347,
y: 448.98375737190554,
parameters: {
fillColor: '#E8E8E8',
size: 40,
strokeWidth: 2,
strokeColor: '#454545',
strokeEnabled: true,
},
ports: [],
children: [],
},
],
},
],
},
{
id: '5d631656-dea2-b5d7-f5ee-ee27e220463b',
type: 'States',
x: 232.07664638676232,
y: 376.3848345864951,
parameters: {
states: 'On,Off',
values: 'Knob X,Background Color',
'type-Background Color': 'color',
'value-On-Knob X': 40,
'value-Off-Knob X': 0,
'value-Off-Background Color': '#F0F0F0',
'value-On-Background Color': '#CCE6CE',
startState: 'Off',
},
ports: [],
children: [],
},
],
connections: [
{
fromId: '5d631656-dea2-b5d7-f5ee-ee27e220463b',
fromProperty: 'Knob X',
toId: 'c66997cc-19d2-630c-692e-0caafdf37dd7',
toProperty: 'transformX',
},
{
fromId: '5d631656-dea2-b5d7-f5ee-ee27e220463b',
fromProperty: 'Background Color',
toId: 'f2461a9d-49c6-f184-1ac1-a75450a7c856',
toProperty: 'backgroundColor',
},
{
fromId: 'f2461a9d-49c6-f184-1ac1-a75450a7c856',
fromProperty: 'onClick',
toId: '5d631656-dea2-b5d7-f5ee-ee27e220463b',
toProperty: 'toggle',
},
],
}}
/>
</div>
## Transitions
For _number_ and _color_ types the **States** node will try to smoothly transition between the states. Sometimes this is not the desired behaviour, and sometimes you want to change how the transition behaves. Noodl let's you edit the transition curves to change the transition behaviour. Take a look at the example below. You can copy the nodes and replace the old nodes in your project or create a new component.
<div className="ndl-image-with-background xl">
<img
src="/2.8/docs/guides/user-interfaces/states/hover-fx-nodes.png"
className="ndl-image med"
></img>
<CopyToClipboardButton
json={{
nodes: [
{
id: '4c6ec6bd-2ba3-807a-9177-da451a835ec2',
type: 'Group',
x: 410.63681490171115,
y: 392,
parameters: { backgroundColor: '#FFFFFF' },
ports: [],
children: [
{
id: '56e1ec26-a280-b297-baf3-f5365f6a6124',
type: 'Group',
x: 430.63681490171115,
y: 438,
parameters: {
width: { value: 80, unit: 'px' },
height: { value: 80, unit: 'px' },
backgroundColor: '#E6DD39',
borderRadius: 20,
alignX: 'center',
marginTop: { value: 70, unit: 'px' },
},
ports: [],
children: [],
},
],
},
{
id: '34a39ad5-0999-133a-5f7e-6943ebd8d899',
type: 'States',
label: 'Hover',
x: 177.3701941946174,
y: 432.9317475959747,
parameters: {
states: 'Yes,No',
startState: 'No',
values: 'Size,Color',
'type-Color': 'color',
'value-Yes-Color': '#D4CE35',
'value-No-Color': '#323975',
'value-Yes-Size': 1.5,
'value-No-Size': 1,
},
ports: [],
children: [],
},
],
connections: [
{
fromId: '56e1ec26-a280-b297-baf3-f5365f6a6124',
fromProperty: 'hoverStart',
toId: '34a39ad5-0999-133a-5f7e-6943ebd8d899',
toProperty: 'to-Yes',
},
{
fromId: '56e1ec26-a280-b297-baf3-f5365f6a6124',
fromProperty: 'hoverEnd',
toId: '34a39ad5-0999-133a-5f7e-6943ebd8d899',
toProperty: 'to-No',
},
{
fromId: '34a39ad5-0999-133a-5f7e-6943ebd8d899',
fromProperty: 'Color',
toId: '56e1ec26-a280-b297-baf3-f5365f6a6124',
toProperty: 'backgroundColor',
},
{
fromId: '34a39ad5-0999-133a-5f7e-6943ebd8d899',
fromProperty: 'Size',
toId: '56e1ec26-a280-b297-baf3-f5365f6a6124',
toProperty: 'transformScale',
},
],
}}
/>
</div>
This simple graph has a **States** node that controls the hover state of a **Group**. As you can see the **Group** sends a signal when it is hovered which transitions the **States** node to the **Yes** state. A different signal is sent when the mouse leaves (you stop hovering), which changes the **States** node to the **No** state. The **States** node changes the **Scale** and **Color** of the **Group** node when it goes between its states. The effect is shown below:
<div className="ndl-image-with-background">
<img
src="/2.8/docs/guides/user-interfaces/states/hover-fx1.gif"
className="ndl-image small"
></img>
</div>
Both transitions (color and size) have the default transition curves. You can edit the transition curves in the properties of the **States** node. Look for the transitions when moving to the **Yes** state (i.e. the hovered state).
<div className="ndl-image-with-background">
<img
src="/2.8/docs/guides/user-interfaces/states/transition-props.png"
className="ndl-image med"
></img>
</div>
The **Default** property is a transition curve that is used for all values if they don't have a specific transition curve set for themselves. If you want to change the curve for all transitions use this.
In this case we want to change the curve for the **Size** transition. You can edit the curve via the curve editor.
<div className="ndl-image-with-background l">
<img
src="/2.8/docs/guides/user-interfaces/states/change-size-curve.gif"
className="ndl-image med"
></img>
</div>
You can play with different settings for the curves and see them working when you hover the rectangle in the preview. Here is one example with a little bounce in the **Size** transition and a linear **Color** transition. Don't forget that you can use the preset icons on the right in the curve editor to choose between four presets:
- **Ease In** Slower in the beginning and accelerating towards the end.
- **Ease Out** Decelerates towards the end.
- **Ease In and Out** Smooth/slower start and end.
- **Linear** Linear animation, starts instantly and keeps constant speed.
<div className="ndl-image-with-background">
<img
src="/2.8/docs/guides/user-interfaces/states/hover-fx2.gif"
className="ndl-image small"
></img>
</div>
## Chaining States
You can also connect several **States** nodes together to create more complex types of animations. In the node graph below we have a second **States** node that expands the **Group** node (changes the width). The second **States** node labeled **Expand** changes the width between the states **Yes** and **No**. The neat thing is that it is triggered when the first states node (the hover states) has reached the **Yes** state. It is returned to **No** when the mouse leaves (the hover end), just like the first node.
<div className="ndl-image-with-background xl">
<img src="/2.8/docs/guides/user-interfaces/states/chaining-states.png"></img>
<CopyToClipboardButton
json={{
nodes: [
{
id: '31b24415-e1ef-7480-db67-f8c3d9df8965',
type: 'Group',
x: 410.63681490171115,
y: 392,
parameters: { backgroundColor: '#FFFFFF' },
ports: [],
children: [
{
id: '2255f3f8-c7c6-a195-9e20-22204968c916',
type: 'Group',
x: 430.63681490171115,
y: 438,
parameters: {
width: { value: 80, unit: 'px' },
height: { value: 80, unit: 'px' },
backgroundColor: '#E6DD39',
borderRadius: 20,
alignX: 'center',
marginTop: { value: 70, unit: 'px' },
},
ports: [],
children: [],
},
],
},
{
id: 'dadabea3-2a7f-ca23-a71d-79fae0cfff6e',
type: 'States',
label: 'Hover',
x: 173.3701941946174,
y: 343.9317475959747,
parameters: {
states: 'Yes,No',
startState: 'No',
values: 'Size,Color',
'type-Color': 'color',
'value-Yes-Color': '#D4CE35',
'value-No-Color': '#323975',
'value-Yes-Size': 1.5,
'value-No-Size': 1,
'transition-Yes-Size': {
curve: [0, 0, 0.39, 2.31],
dur: 300,
delay: 0,
},
'transition-Yes-Color': {
curve: [0, 0, 1, 1],
dur: 300,
delay: 0,
},
'transition-No-Color': {
curve: [0, 0, 1, 1],
dur: 300,
delay: 0,
},
},
ports: [],
children: [],
},
{
id: '82a0d318-ac00-eb50-653e-bf2aa4344425',
type: 'States',
label: 'Expand',
x: 181.5019645647742,
y: 554.7158737979873,
parameters: {
states: 'Yes,No',
values: 'Width',
'value-Yes-Width': 200,
'value-No-Width': 90,
startState: 'No',
'transition-Yes-Width': {
curve: [0, 0, 0.58, 1],
dur: 500,
delay: 0,
},
},
ports: [],
children: [],
},
],
connections: [
{
fromId: '2255f3f8-c7c6-a195-9e20-22204968c916',
fromProperty: 'hoverStart',
toId: 'dadabea3-2a7f-ca23-a71d-79fae0cfff6e',
toProperty: 'to-Yes',
},
{
fromId: '2255f3f8-c7c6-a195-9e20-22204968c916',
fromProperty: 'hoverEnd',
toId: 'dadabea3-2a7f-ca23-a71d-79fae0cfff6e',
toProperty: 'to-No',
},
{
fromId: 'dadabea3-2a7f-ca23-a71d-79fae0cfff6e',
fromProperty: 'Color',
toId: '2255f3f8-c7c6-a195-9e20-22204968c916',
toProperty: 'backgroundColor',
},
{
fromId: 'dadabea3-2a7f-ca23-a71d-79fae0cfff6e',
fromProperty: 'Size',
toId: '2255f3f8-c7c6-a195-9e20-22204968c916',
toProperty: 'transformScale',
},
{
fromId: '82a0d318-ac00-eb50-653e-bf2aa4344425',
fromProperty: 'Width',
toId: '2255f3f8-c7c6-a195-9e20-22204968c916',
toProperty: 'width',
},
{
fromId: 'dadabea3-2a7f-ca23-a71d-79fae0cfff6e',
fromProperty: 'reached-Yes',
toId: '82a0d318-ac00-eb50-653e-bf2aa4344425',
toProperty: 'to-Yes',
},
{
fromId: '2255f3f8-c7c6-a195-9e20-22204968c916',
fromProperty: 'hoverEnd',
toId: '82a0d318-ac00-eb50-653e-bf2aa4344425',
toProperty: 'to-No',
},
],
}}
/>
</div>
This will result in the behaviour shown below. As you can see the first **States** node triggers the bouncy size change, which is immediately followed by the second **Expand** states node that changes the state to **Yes** and transitions the width to the expanded state.
<div className="ndl-image-with-background">
<img
src="/2.8/docs/guides/user-interfaces/states/hover-fx3.gif"
className="ndl-image small"
></img>
</div>
The **States** node is one of the most commonly used nodes in Noodl apps, and mastering it will let you create many great interactions. Have fun!

View File

@@ -0,0 +1,162 @@
---
title: Style Variants
hide_title: true
---
# Style Variants
## What you will learn in this guide
When building apps its very common that we need the same styling in multiple places. This guide will take you through the most common method of reusing styles, using the Style Variants feature.
## Overview
We will go through the following steps in this guide:
- Create a Style Variant
- Learn the differences between overrides and variants
- Edit a Style Variant
There is also a video walking through the guide.
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/zFF8hoC-JM8" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe>
## The quick and dirty method
We could just copy and paste a node we styled, but that comes with a couple of downsides.
First of all, we would have to search through all of our pages and components looking for the styled visual node we want whenever we need to include it in a new place.
Similarly, if we would find that the design needs to be tweaked, we would have to go through all pages and components, manually updating every instance of this copied node.
The better way to achieve reusability and maintainability is by using Style Variants.
## Creating a Style Variant
Lets see it in action. In our design system we have a “Primary” button used to indicate the main actions, and a “Secondary” button that is a bit more discreet. Lets build them, and use them in our app.
We start by creating a button in the node tree.
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/style-variants/create-button.png)
</div>
We will add a bit more left and right padding, increase the height and make the corners rounder.
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/style-variants/button.png)
</div>
That looks good. Now lets create another button.
<div className="ndl-image-with-background s">
![](/docs/guides/user-interfaces/style-variants/second-button.png)
</div>
Oh, wait. Why does the new button not have any of the changes that we made? This is because we didnt save the changes as a variant, so they became overrides on this specific instance of the button, instead of being attached to all the Button nodes.
Lets fix this and save our changes as a variant so that we can start reusing it.
At the top of our restyled buttons Property Panel, click the “plus”, “Create new variant”, give it a name and confirm.
<div className="ndl-image-with-background l">
![](/docs/guides/user-interfaces/style-variants/create-variant.png)
</div>
This will take all the overrides, remove them from the current instance and save them as a new variant together with all other non-overridden properties.
We can now select the second button and set it to use the "Primary" variant.
<div className="ndl-image-with-background l">
![](/docs/guides/user-interfaces/style-variants/set-variant.png)
</div>
<div className="ndl-image-with-background s">
![](/docs/guides/user-interfaces/style-variants/styled-buttons.png)
</div>
## Editing a Style Variant
Now that we have the “Primary” button, lets create the “Secondary” variant.
For demonstrational purposes I will show you another workflow so that you can pick the one you like the best. This method will also cover how to edit variants.
Since the “Secondary” variant is very similar to the “Primary” one we will create a button and set it to use the “Primary” variant. This way we dont have to repeat any of the styling weve already done.
<div className="ndl-image-with-background l">
![](/docs/guides/user-interfaces/style-variants/set-variant.png)
</div>
<div className="ndl-image-with-background s">
![](/docs/guides/user-interfaces/style-variants/styled-buttons.png)
</div>
This time we will not override any styles though. Instead well immediately create a new variant and name it “Secondary”.
<div className="ndl-image-with-background l">
![](/docs/guides/user-interfaces/style-variants/new-variant.png)
![](/docs/guides/user-interfaces/style-variants/name-new-variant.png)
</div>
We now have a “Secondary” variant that is identical to the “Primary” one.
As you already know, if we change any properties now it will count as overrides, and only be saved to this particular instance of the Button node. So, in order to edit the Variant and not the Instance, we press the “Edit variant” button.
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/style-variants/edit-variant.png)
</div>
Notice how the borders of the Property Panel become a helpful teal color. This is so that we remember that we are in the Variant editor.
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/style-variants/helpful-teal.png)
</div>
Lets give this variant a “Primary” colored text, a transparent background and a “Primary” colored border.
<div className="ndl-image-with-background s">
![](/docs/guides/user-interfaces/style-variants/two-buttons.png)
</div>
In the “Visual States” guide I got some feedback from my designer friend that I forgot to style the hover state, so lets make them happy and update the hover styles this time. Lets make the background a nice “Primary Light” color. All of the visual states are tied to a Variant and saved on it.
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/style-variants/visual-state.png)
</div>
All changes done when editing a Variant are saved automatically, so we dont have to do anything else now that were done with the styling, but if we for some reason want to go back to editing the instance, we can click the “Close” button to exit the Variant editor.
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/style-variants/close.png)
</div>
We can now populate our whole app with identical buttons, making sure that they always follow the styling defined in our brand guidelines. And if the guidelines would change, we can quickly edit a variant and have it be updated across our whole app.

View File

@@ -0,0 +1,70 @@
---
title: Visual States
hide_title: true
---
# Working with Visual States
## What you will learn in this guide
This guide will teach you how to style **Visual nodes** differently based on user interaction.
## Overview
We will go through the following steps in this guide:
- Create a button
- Style its `neutral` state
- Style it differently in its `hover` state
There is also a video walking through the guide.
<iframe width="560" height="315" src="https://www.youtube.com/embed/ATyqeK_deu4" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe>
## Let's go!
When working with Visual nodes we often find ourselves in situations where we want to style the node differently based on the user interaction. We might want to have a different background color when hovering a button, or indicating that a checkbox has been checked. This is done using **Visual States** in the nodes settings in the **Property Panel**.
Lets create a button in our node tree, and give it a quick styling.
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/visual-states/node-created.png)
</div>
Our brand guidelines tell us that all buttons need to be the Dark color and that they shouldn't have any rounded corners.
<div className="ndl-image-with-background s">
![](/docs/guides/user-interfaces/visual-states/wrong-hover.gif)
</div>
Ok, so, it looks good now, but when I sent it to my designer friend they told me that while the button looked good in the neutral state, it was all wrong when hovered. The button is supposed to be the Primary Light color, and the text should be Dark. Lets fix that by modifying the Hover state.
With the Button node still selected, lets move over to the Visual State selector and click the Hover option.
<div className="ndl-image-with-background">
![](/docs/guides/user-interfaces/visual-states/states.png)
</div>
We will see the Property Panel shift around a bit. Some properties just don't make any sense to change in an interaction state, so Noodl will hide those properties for us.
Lets scroll down to the properties for Label and Background Color and update them.
<div className="ndl-image-with-background l">
![](/docs/guides/user-interfaces/visual-states/update-states.png)
</div>
And voila - we are now following the brand guidelines.
<div className="ndl-image-with-background s">
![](/docs/guides/user-interfaces/visual-states/right-hover.gif)
</div>

View File

@@ -0,0 +1,256 @@
---
title: Creating users in Noodl
hide_title: true
---
# Creating users in Noodl
<div className="ndl-image-with-background xl">
![](/docs/guides/user-management/creating-users-in-noodl/intro.png)
</div>
## What you will learn in this guide
This guide will take you through the process of creating and managing user accounts in your Noodl apps. To support user accounts, make sure that you already have a [Noodl cloud service](/docs/guides/cloud-data/creating-a-backend) enabled on your project or have your [own backend attached](/docs/guides/deploy/using-an-external-backend).
### Overview
We will go through the following topics in this guide:
- Overview of the **User** nodes
- Using the **Sign Up / Login template**
- Creating a simple sign up page
- Creating a simple log in page
- Display user data
- Log out a user
## The User nodes
<div className="ndl-image-with-background">
![](/docs/guides/user-management/creating-users-in-noodl/user_nodes.png)
</div>
Noodl comes with several built-in nodes for managing users in your application. These nodes will take care of all the backend operations needed to manage users and can be integrated flexibly into the user experience you are building. With these nodes, you can log in existing users, log out a user, sign up new users, get user information, and update user properties. Read more about the nodes on the Node Reference page:
- [Log in](/nodes/data/user/log-in)
- [Log Out](/nodes/data/user/log-out)
- [Sign Up](/nodes/data/user/sign-up)
- [User](/nodes/data/user/user-node)
- [Set User Properties](/nodes/data/user/set-user-properties)
## The Sign up & Login Template
<div className="ndl-image-with-background xl">
![](/docs/guides/user-management/creating-users-in-noodl/template.png)
</div>
The easiest way to get what you need to create an app that supports user accounts is to start from the **Signup & Login Template** that you can find when you create a new project in Noodl. This template provides you with all the pages and logic needed in app that handles user accounts. This includes:
1. Sign up and login users.
2. Send verification emails using [Noodl Cloud Functions and SendGrid](/docs/guides/cloud-logic/email-verification),
3. Resetting passwords, and other user data via an account page
Note that you will need a [SendGrid](https://sendgrid.com/) API key to send emails to users in this template. If you prefer another email client you can provide your own.
In the rest of this guide we will take a closer look at the core patterns for creating a sign up and log in flow step-by-step.
## Creating A Sign Up Page
Most apps that have user accounts have a sign-up flow, where users can go and sign up. To create this we need the following:
1. A page in our [page router](/nodes/navigation/page-router) that holds the sign-up interface
2. An interface with a form to capture the users email, and password
3. The [Sign Up](/nodes/data/user/sign-up) and [Navigate node](/nodes/navigation/navigate)
<div className="ndl-image-with-background">
![](/docs/guides/user-management/creating-users-in-noodl/createaccountpage.png)
</div>
Let's start by [creating a new page](/docs/guides/navigation/basic-navigation/#pages) called **Sign Up Page** and click on the page component to add elements to the page. First, lets open the node picker and add a [Group node](/nodes/basic-elements/group) to our Page Node, so we can better can control the layout of the page.
When we start adding visual elements to build our sign up form, we want to have it in the middle of the page, so lets style the Group node a bit:
Let's select the Group node and set its **max width** to 400px.
<div className="ndl-image-with-background">
![](/docs/guides/user-management/creating-users-in-noodl/max-width.png)
</div>
Then let's center the Group horizontally on the page by setting its alignment property to **Horizontal Align Center**.
<div className="ndl-image-with-background">
![](/docs/guides/user-management/creating-users-in-noodl/centeralign.png)
</div>
Lastly let's center all content inside of the Group vertically.
<div className="ndl-image-with-background">
![](/docs/guides/user-management/creating-users-in-noodl/content-center.png)
</div>
### Creating A Sign Up Form
Now let's add some elements! Let's add a text Node to give a title to the page that says ***Create Account*** and give it a text style of **Title Large**. Next lets add three [Text Input nodes](/nodes/ui-controls/text-input) to capture username, email, and password. A username is optional, whereas email and password are required for the Sign Up node to succeed.
<div className="ndl-image-with-background xl">
![](/docs/guides/user-management/creating-users-in-noodl/signupformelements.png)
</div>
Lets make the first Text Input for email be changing its **Type** to **Email** and its **Label** to **Email**. Next, lets change the **Type** property of the last text inputs to **Password**, to make it hide the characters that a user writes.
<div className="ndl-image-with-background">
![](/docs/guides/user-management/creating-users-in-noodl/textinput-type.png)
</div>
### Using the Sign Up node
Now lets add a [Sign Up node](/nodes/data/user/sign-up) to the node graph, and connect the three Text Input nodes to the corresponding inputs on the Sign-up node.
<div className="ndl-image-with-background xl">
![](/docs/guides/user-management/creating-users-in-noodl/sign-up-node.png)
</div>
Lastly, we need a button to execute the user creation, so let's add a [Button Node](/nodes/ui-controls/button) below our text inputs and connect its **Click** output to the **Do** input on the Sign Up node.
<div className="ndl-image-with-background xl">
![](/docs/guides/user-management/creating-users-in-noodl/create-account-button.png)
</div>
### See users in the backend
If we now fill in the form and click the button, the Sign Up node will execute and create a new user in the backend. If we open the [Cloud Service dashboard](/docs/guides/cloud-data/creating-a-backend#inspect-the-cloud-service-using-the-dashboard) for the the logged in user, we can see the created users in the User Class.
<div className="ndl-image-with-background xl">
![](/docs/guides/user-management/creating-users-in-noodl/dashboard.png)
</div>
### Navigate when a new user is created
When the Sign Up node creates a new user it also logs in that user, so when that happens, we want to navigate to a new page to show the user that they have signed up. But before we can this, we need a page to navigate to.
Let's create a new page called ***Home*** and then go back to our Sign Up Page component and add a Navigate node next to our Sign up node. The Sign Up node sends a signal if it succeeds, and we can use that signal to perform the **do** action on the Navigate node.
<div className="ndl-image-with-background xl">
![](/docs/guides/user-management/creating-users-in-noodl/navigate-onsignup.png)
</div>
If we try to sign up a new user now, we can see that it navigates ot the Home page.
Let's now make a login page, so that existing users have a way to log in.
## Creating a Log in Page
Let's start by creating a new page called **Log In Page** and set that to be the [start page](/nodes/navigation/page-router/#start-page) in the Page Router node in the App component.
<div className="ndl-image-with-background xl">
![](/docs/guides/user-management/creating-users-in-noodl/makestartpage.png)
</div>
Now lets copy all the visual nodes from the Sign Up page and place them in the New Login Page, as we can reuse most of the layout and Input fields.
Then Delete the Username Text Input, and change the title text node and the button to say "Log in".
<div className="ndl-image-with-background xl">
![](/docs/guides/user-management/creating-users-in-noodl/login-page.png)
</div>
Next, Let's add a [Log In node](/nodes/data/user/log-in), and hook it up as shown on the image bellow.
<div className="ndl-image-with-background xl">
![](/docs/guides/user-management/creating-users-in-noodl/login-node.png)
</div>
Similar to the Sign up flow, we want to navigate when the user logs in. But another trick we can do here is to use the [User node](/nodes/data/user/user-node) to check if there is already a logged in user in memory. This is something the User node keeps track of for us, and we can use its Authorized output to perform the navigate if it's returning True.
As we have the Log In Page as our start page, we can use the User node here together with a [Condition node](/nodes/utilities/logic/condition) to navigate if the user is already logged in from a previous session. Add a User node, Condition node, and a Navigate and connect it up as shown on the image bellow. The Navigate node should target the Home Page.
<div className="ndl-image-with-background xl">
![](/docs/guides/user-management/creating-users-in-noodl/autologin.png)
</div>
## Show user data
If we want to show user related data we can as well use the User node, to get the data once a user has logged in. Let's add a small greeting on the Home Page to show the logged in user's name. We can do that by using the User node together with a [String Format node](/nodes/string-manipulation/string-format) as shown bellow.
<div className="ndl-image-with-background xl">
![](/docs/guides/user-management/creating-users-in-noodl/homescreen.png)
</div>
## Log out a user
Lastly we can also log out a user by using the [Log Out node](/nodes/data/user/log-out). We need an event to trigger the log out so for this demonstration let's add a button and hook it's **Click** output to the **Do** input on the Sign Out node as the image shows bellow. Note that when login in users, it doesnt matter if we connect the email or username to the **Username** input on the Log In node. Both will work.
<div className="ndl-image-with-background xl">
![](/docs/guides/user-management/creating-users-in-noodl/logout.png)
</div>
Again, we need to perform a navigation action once a user logs out, so lets add a Navigate node that takes the user to the log in page when they sign out. We can achieve this by connecting the **success** signal from the Log Out node to the **Do** input on the navigate node and have it targeting the Log In Page.
<div className="ndl-image-with-background xl">
![](/docs/guides/user-management/creating-users-in-noodl/navigate-logout.png)
</div>
Let's end this guide by adding a few buttons to navigate between the sign up and login page. We can do that with another button that we style as a text button connected to a navigate node.
So on the Log In page we want a button that performs a navigate to the Signup page.
<div className="ndl-image-with-background xl">
![](/docs/guides/user-management/creating-users-in-noodl/login-signup-navigate.png)
</div>
And on the Sign up page we want a button to navigate from Sign up Page to the Log In page.
<div className="ndl-image-with-background xl">
![](/docs/guides/user-management/creating-users-in-noodl/signup-login-navigate.png)
</div>
And now we have a simple flow to sign up users and log them in. Next steps would be to add form validation to the text inputs, which can be done with the [Form Validation module](/library/modules/validation), and sending verification emails when a user signs up. From here we encourage you to create a new project using the **Sign up and login template** to inspect more patterns on how to do that.

View File

@@ -0,0 +1,14 @@
---
title: Navigation in Noodl Overview
hide_title: true
---
import ReactPlayer from 'react-player'
# Navigation in Noodl
A central part of any App is a navigation system. Noodl has a super flexible navigation system where you can build both classic Web Style navigation, app style navigation and Popups.
<ReactPlayer playing autoplay muted loop url='overview/signup.mp4' />
### [Start learning about navigation](/docs/guides/navigation/basic-navigation)

View File

@@ -0,0 +1,344 @@
---
title: Filter your Table data
hide_title: true
---
import CopyToClipboardButton from '/src/components/copytoclipboardbutton'
import ImportButton from '../../../src/components/importbutton'
# Filter your Table data
## What you will learn in this guide
This guide continues building on the [Using the Table node to display data](/docs/guides/visualizing-data/table-to-visualize-data) and the [Adding pagination to the Table](/docs/guides/visualizing-data/table-pagination) guides. In those two guides we have created a **Table** with pagination that displays data on National Parks in the US. In this guide we will use the **[Filters](/library/prefabs/filters/)** component prefab so that we can filter the data and have the **Table** display only the parks that fit the filter.
## Some additional layouting
Before we add the **Filters** prefab let's add some more **Groups** so that we get a nice layout for both the **Table** and the **Filters**.
Add a **Group** directly under the **Page** and set it's **Layout** property to be Horizontal. Let's also name it Horizontal Group so that we know what it is. Below are two screenshots showing where the Horizontal Group is placed in the node graph and it's properties.
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/filter-table-data/horizontal-group.png)
</div>
<div className="ndl-image-with-background s">
![](/docs/guides/visualizing-data/filter-table-data/horizontal-group-props.png)
</div>
Add another **Group** as a child to the Horizontal Group, and set it's **Width** to 300px. Let's also givie it 40px **Right Margin**, and set it's **Position** in the Layour section to be Sticky. Name it Filter Column, it should look like the screenshots below:
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/filter-table-data/filter-column.png)
</div>
<div className="ndl-image-with-background s">
![](/docs/guides/visualizing-data/filter-table-data/filter-column-props.png)
</div>
As a last step, move the **Group** that contains our **Table** and related nodes, so it is a sibling to the Filter Column.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/sibling.png)
</div>
## Import the Filters component
Bring up the Node Picker, select the prefabs tab, and find the **Filters** prefab. Click the Clone button to bring it into your project.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/clone-filter.png)
</div>
Let's add the **Filters** component as a child to the Filter Column **Group**.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/add-filters.png)
</div>
## Hooking up Filters to Query Records
Before we define the exact filters we want to have, let's prepare the **Query Records** node to take in the filters from the **Filters** component and hook it all up.
Select the Query Records node and change the **Filter** proeprty in the General section to be Javascript:
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/javascript-filters-qr.png)
</div>
Then click the Edit button by the **Filter** property in the Filter section for the **Query Records** node and input the following code:
```javascript
where(Inputs.filters);
```
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/edit-filters-qr.png)
</div>
This gives us an input that takes filters from the **Filters** component, so let's hook it up like this:
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/filters-qr-connections.png)
</div>
- Connect <span class="ndl-data">Filter</span> from the **Filters** to <span class="ndl-data">Filters</span> in the **Query Records**
- Connect <span class="ndl-signal">Filter Changed</span> from the **Filters** to <span class="ndl-signal">Do</span> in the **Query Records**
You should now see the default filters from the **Filters** component in your preview, and in the next section we will customize which filters we will use in this app.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/filters-in-preview.png)
</div>
## Customizing the filters
If you look at the **[Filters](/library/prefabs/filters/)** documentation you can see that we have the ability to use 7 different types of filters out of the box. Let's use a Text search filter so that we can search for a specific park, the Multi Choice filter so that we can select one or more specific states, and the range filter so that we can filter the parks based on their size.
The **Filters** component can take an array of filters as an input, so let's use a **Function** node to create an array with our filters. Add a **Function** node, name it Create Filters and add the following code:
```javascript
Outputs.Filter = [
{
Name: 'NameSearch',
Type: 'Text Search',
Label: 'Search',
Property: 'Name',
},
{
Name: 'StateSelector',
Label: 'Show states',
Type: 'Multi Choice',
Options: Inputs.States || [],
Labels: Inputs.States || [],
Property: 'State',
},
{
Name: 'MinMaxSize',
Label: 'Size Filter',
Type: 'Range',
Max: Inputs.maxSize || 35000,
Min: Inputs.minSize || 0,
Step: 100,
Property: 'Size_km2',
Value: 100,
},
];
```
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/create-filters-func.png)
</div>
Before we continue, let's have a look at the Multi Choice filter which is the second object in the array:
```javascript
{
Name:"StateSelector",
Label:"Show states",
Type:"Multi Choice",
Options:Inputs.States||[],
Labels:Inputs.States||[],
Property:"State"
}
```
Here we have defined an `Inputs.States`, and the idea is that we will provide this function node with a list of distinct states that are available in the Parks data. If `Inputs.States` is undefined, we just return an empty array, that is what this line does: `Inputs.States||[]` (The same pattern is used for the max and min size). So until we have a list of states, the Multi Choice filter will be empty. Let's hook up the **Function** node to the **Filters** so that we can see what we get:
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/custom-filters-preview.png)
</div>
Try out the search and range filter, it should already work.
## Getting a distinct list of states and min and max size
To get a list of distinct states and the minimum and maximum size from the data we have in the Parks class we can use a **Cloud Function** and some Javascript. We will use the [Noodl.Records.distinct()](/javascript/reference/records/#noodlrecordsdistinctclassnamepropertyquery) function and the [Noodl.Records.aggregate()](/javascript/reference/records#noodlrecordsaggregateclassnameaggregatesquery) and it is only available for use in a Cloud Function so let's create one of those.
Select the Cloud Functions tab.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/cloud-tab.png)
</div>
Click the plus sign and create a new Cloud Function Component and call it Get States.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/create-cloud-func.png)
</div>
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/get-states-func.png)
</div>
Next select the **Request** node and check the **Allow Unauthenticated** property. This is so that we can call the Cloud Function without having a signed in user.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/request-node.png)
</div>
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/request-allow.png)
</div>
Now let's add a **Function** node and copy in the following code:
```javascript
try {
Outputs.states = await Noodl.Records.distinct('Parks', 'State');
const minMax = await Noodl.Records.aggregate('Parks', {
minSize: { min: 'Size_km2' },
maxSize: { max: 'Size_km2' },
});
Outputs.maxSize = minMax.maxSize;
Outputs.minSize = minMax.minSize;
Outputs.success();
} catch (e) {
Outputs.errorMessage = e;
Outputs.error();
}
```
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/distinct-states.png)
</div>
The code pulls out all distinct states from the Parks class and make them available in the `Outputs.states`. We also get the minimum and maximum size in an object that we call minMax, that we then make available as outputs in the `Outputs.maxSize` and `Outputs.minSize`, and send a success signal if it is successful. If there is an error it will give us an error message and send an error signal. Let's first handle the success case. Select the **Response** node and name it Success Response and then add parameters called states, Max Size, and Min Size.
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/filter-table-data/success-resp.png)
</div>
Then from the **Function** node make the connections like in the image below.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/success-resp-connection.png)
</div>
Bring up the Node Picker and add a second **Response** node. Call it Error Response, and change the **Status** property to Failure, then connect it like in the second image.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/error-resp.png)
</div>
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/error-resp-connection.png)
</div>
Now we just need to connect the <span class="ndl-signal">Received</span> signal from the **Request** node to the <span class="ndl-signal">Run</span> action of the **Function** node.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/cloud-func-final.png)
</div>
That's it for the Cloud Function, now let's use it in our Start Page.
## Add Cloud Function to Start Page
Go back to your Start Page component, and add a **Cloud Function** node from the Node Picker.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/add-cf-in-start.png)
</div>
Select the Cloud Function node and from the Function dropdown select the Get States function.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/cf-props.png)
</div>
We want the **Cloud Function** to execute as soon as the page becomes visible so let's connect the <span class="ndl-signal">Did Mount</span> signal from the **Page** node to the <span class="ndl-signal">Received</span> signal.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/cf-did-mount.png)
</div>
Next connect the **Cloud Function** to the **Function** node like in the image below:
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/get-states-to-func.png)
</div>
Let's also empty out the default filters from the **Filters** component by selecting the **Filters** property and leaving an empty array like in the screenshot:
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/empty-filters.png)
</div>
Now your final Node Graph should look like this:
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/final-node-graph.png)
</div>
And your app should have selections for states and fully functioning filters:
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/filter-table-data/final-filters-result.png)
</div>
## Summary
In this guide we used the **[Filters](/library/prefabs/filters/)** component prefab to help us filter the data our **Table** shows. We also used a **Cloud Function** to pull out a list of distinct states from our data in the backend.

View File

@@ -0,0 +1,297 @@
---
title: Styling the Table
hide_title: true
---
import CopyToClipboardButton from '/src/components/copytoclipboardbutton'
import ImportButton from '../../../src/components/importbutton'
# Styling the Table
## What you will learn in this guide
In this guide we will look at how we can style the **[Table](/library/prefabs/table/)** prefab and make the app we have created in the previous guides look a bit nicer. We will only make some minor changes to the background colors and borders of the **Table**, but we encourage you to play around with the **Table** prefab on your own and make it into something that fits your use cases.
## Changing the app background
Let's start with an easy change, the overall background color of our app. Go the the App component and select the **Page Router**.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/styling-table/select-app.png)
</div>
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/styling-table/select-pr.png)
</div>
Now change the **Background Color** property to ```#F0EDE8```, and the app should look like the second image below.
<div className="ndl-image-with-background s">
![](/docs/guides/visualizing-data/styling-table/change-bg-pr.png)
</div>
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/styling-table/new-bg.png)
</div>
## Styling the table
Go back to the Start Page and double click on the **Table** component in the node graph. It should take you into the **Table** prefab, and this is where we will make some styling changes.
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/styling-table/double-click-table.png)
</div>
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/styling-table/table-overview.png)
</div>
Select the top **Group** and remove it's **Border** by setting it to none and set the **Corner Radius** to 0:
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/styling-table/select-table-group.png)
</div>
<div className="ndl-image-with-background m">
![](/docs/guides/visualizing-data/styling-table/table-group-props.png)
</div>
Next select the **Group** named Header Row and find the Border Style section. In the Border Style section, select the bottom border and set it's color to Grey - 300. Let's also set it's **Background Color** to White.
<div className="ndl-image-with-background m">
![](/docs/guides/visualizing-data/styling-table/select-header-row.png)
</div>
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/styling-table/header-row-props.png)
</div>
It doesn't look like much right now, but let's continue by styling the rows of the **Table**. In the Components tab, unfurl the **Table** and select the **Row** subcomponent.
<div className="ndl-image-with-background m">
![](/docs/guides/visualizing-data/styling-table/row-in-comp.png)
</div>
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/styling-table/row-overview.png)
</div>
We want all rows to have the same background color so select the **Color Blend** node and set **Color 0** and **Color 1** to Grey - 100.
<div className="ndl-image-with-background m">
![](/docs/guides/visualizing-data/styling-table/color-blend-props.png)
</div>
Also make sure that the bottom border of the **Group** named Row is set to 1px and Grey - 300
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/styling-table/row-border-props.png)
</div>
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/styling-table/table-in-progress.png)
</div>
The table is starting to look pretty good, we want to add a border around the whole thing, but before we do that, let's style the **Image Cell** slightly.
Select the **Image Cell** in the Components view.
<div className="ndl-image-with-background m">
![](/docs/guides/visualizing-data/styling-table/image-cell.png)
</div>
Find the **Image** node and give it 8px Corner Radius.
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/styling-table/image-props.png)
</div>
Now we will add a border around the whole **Table** and we will also include the **Pages And Rows** in that border. Go back to the Start Page, and wrap the **Table** and **Pages And Rows** in a **Group** called Table Border.
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/styling-table/table-border-group.png)
</div>
Select the Table Border **Group** and set the **Border style** to Solid 1px and Grey - 300, give it 8px **Corner Radius** and make sure that **Clip Content** is checked:
<div className="ndl-image-with-background m">
![](/docs/guides/visualizing-data/styling-table/table-border-props.png)
</div>
The table now looks really good, except for the bottom where we have the **Pages And Rows**.
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/styling-table/table-style-done.png)
</div>
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/styling-table/table-bottom.png)
</div>
Let's fix it by wrapping the **Pages And Rows** in it's own **Group** and give that Group a White **Background**.
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/styling-table/pages-and-rows-in-group.png)
</div>
Select the **Pages And Rows** and give it 16px margin all around.
<div className="ndl-image-with-background s">
![](/docs/guides/visualizing-data/styling-table/pages-and-rows-padding.png)
</div>
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/styling-table/table-final.png)
</div>
That looks quite nice, so let's make the Filters section look a bit nicer too.
## Style the Filters section
The filters section currently looks like this:
<div className="ndl-image-with-background m">
![](/docs/guides/visualizing-data/styling-table/filters-initial.png)
</div>
It needs a header so let's add a **Group** as a child to the Filter Column and call it Filter Header. Then with the Filter Header **Group** selected set the **Height** to 42px and have it align it's content vertically centered. Also give it 16px **Padding** both left and right and set it's **Background Color** to White.
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/styling-table/add-filter-header.png)
</div>
<div className="ndl-image-with-background s">
![](/docs/guides/visualizing-data/styling-table/filter-header-props.png)
</div>
<div className="ndl-image-with-background s">
![](/docs/guides/visualizing-data/styling-table/filter-header-props-bg.png)
</div>
Add **Text** node as a child to the Filter Header **Group** and set the **Text** property to "Filters" and change the **Text Style** to Label Medium.
<div className="ndl-image-with-background s">
![](/docs/guides/visualizing-data/styling-table/text-node-filter-header.png)
</div>
<div className="ndl-image-with-background s">
![](/docs/guides/visualizing-data/styling-table/text-node-props.png)
</div>
Wrap the **Filter** component in a **Group** and add 16px Top, Left and Right *Padding** to the Group.
<div className="ndl-image-with-background s">
![](/docs/guides/visualizing-data/styling-table/filters-in-group.png)
</div>
<div className="ndl-image-with-background s">
![](/docs/guides/visualizing-data/styling-table/group-padding-props.png)
</div>
Now select the Filter Column **Group** and update the **Top Margin** to 40px, check the **Clip Content** property, set the **Background Color** to Grey - 100, give it a Solid 1px, Grey - 300 border all around and a **Corner Radius** of 16px.
<div className="ndl-image-with-background s">
![](/docs/guides/visualizing-data/styling-table/select-filter-column.png)
</div>
<div className="ndl-image-with-background s">
![](/docs/guides/visualizing-data/styling-table/column-props-1.png)
</div>
<div className="ndl-image-with-background s">
![](/docs/guides/visualizing-data/styling-table/column-props-2.png)
</div>
That's if for the filters section, but as a final touch we will update the **Text Style** called Body Medium. Select the **Text** node in the Filter Header, find the **Text Style** property and click the settings icon next to Body Medium and set the **Size** to 14px.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/styling-table/text-style-conf.png)
</div>
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/styling-table/text-style-props.png)
</div>
## Summary
Now we have styled our **Table** and the filters section and below you can see what it looked like before and after the styling. Prefabs like that **Table** are a great way to quickly build out your experiences, but they are meant to be tinkered with and hopefully this guide has given you some ideas of how you can update the look and feel of prefabs.
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/styling-table/table-no-style.png)
</div>
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/styling-table/table-with-style.png)
</div>

View File

@@ -0,0 +1,121 @@
---
title: Adding pagination to the Table
hide_title: true
---
import CopyToClipboardButton from '/src/components/copytoclipboardbutton'
import ImportButton from '../../../src/components/importbutton'
# Adding pagination to the Table
## What you will learn in this guide
In this guide we will use the **[Pages And Rows](/library/prefabs/pagesandrows/)** component prefab to add pagination to the **[Table](/library/prefabs/table)** from the [Using the Table node to display data](/docs/guides/visualizing-data/table-to-visualize-data) guide. The **Pages And Rows** node also allows users to control how many rows the **Table** displays on each page.
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/j9sUBnFfjxo" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe>
## Adding the Pages And Rows prefab
Bring up the Node Picker and select the Prefabs tab. Then find the **Pages And Rows** prefab and click clone.
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/table-pagination/clone-pages-rows.png)
</div>
You should now have the **Pages And Rows** component in your project, so let's add it to the node graph underneath the **Table** node in the Start Page, like in the screenshot below:
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-pagination/pages-rows-in-graph.png)
</div>
Let's also give the **Pages And Rows** component a **Top Margin** of 16px:
<div className="ndl-image-with-background s">
![](/docs/guides/visualizing-data/table-pagination/top-margin.png)
</div>
## Setting up the Query Records node
In order to use the **Pages And Rows** component, we need to enable some properties on the **Query Records** node. Check the **Use Limit** property and also check the **Fetch Total Count** property. It should look like this:
<div className="ndl-image-with-background s">
![](/docs/guides/visualizing-data/table-pagination/query-records-properties.png)
</div>
The **Pages And Rows** component will give us the values for **Limit** and **Skip**, and the **Query Records** will give the total count to the **Pages And Rows**. In the next section we will make the connections.
## Connecting the Pages And Rows with the Query Records node
Let's start by making some connections from the **Pages And Rows** component to the **Query Records** node. Make the following connections:
- Connect the <span class="ndl-data">Skip</span> output from the **Pages And Rows** to the <span class="ndl-data">Skip</span> input of the **Query Records**
- Connect the <span class="ndl-data">Limit</span> output from the **Pages And Rows** to the <span class="ndl-data">Limit</span> input of the **Query Records**
- Connect the <span class="ndl-signal">Changed</span> output from the **Pages And Rows** to the <span class="ndl-signal">Do</span> input of the **Query Records**
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-pagination/connections-pr-qr.png)
</div>
Now from the **Query Records** make a connection to the **Pages And Rows**:
- Connect the <span class="ndl-data">Total Count</span> output from the **Query Records** to the <span class="ndl-data">Total Count</span> input of the **Pages And Rows**
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-pagination/connections-qr-pr.png)
</div>
It should look like this in the node graph when you are done:
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-pagination/connections-done.png)
</div>
## Explicit control of when the Query Records node fetches it's data
If you reload the application now, you will be met with the following:
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-pagination/empty-table.png)
</div>
The reason the table looks like this is because the **Query Records** node hasn't fetched any data. When we hooked up the <span class="ndl-signal">Changed</span> signal from the **Pages And Rows** to the <span class="ndl-signal">Do</span> action of the **Query Records**, the **Query Records** node went from implicitly fetching data, to only fetching data when it gets a signal to the <span class="ndl-signal">Do</span> action. This means that we want to make sure that the **Query Records** node fetches data as soon as something in the Node Graph has become visible, so let's connect the <span class="ndl-signal">Did Mount</span> signal from the **Group** node to the <span class="ndl-signal">Do</span> action of the **Query Records**, like this:
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-pagination/group-qr.png)
</div>
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-pagination/all-connections.png)
</div>
If you reload now, it should look like this:
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-pagination/table-with-pagination.png)
</div>
## Summary
Now we have a fully functioning table where the user can control the number of rows that are displayed on each **Table** page thanks to the **Pages And Rows** component.

View File

@@ -0,0 +1,201 @@
---
title: Using the Table node to display data
hide_title: true
---
import CopyToClipboardButton from '/src/components/copytoclipboardbutton'
import ImportButton from '../../../src/components/importbutton'
# Using the Table node to display data
## What you will learn in this guide
In this guide we will walk you through how to use the **[Table](/library/prefabs/table)** node and get it to display data that is retrieved from a **[Query Records](/nodes/data/cloud-data/query-records)** node.
## Create new project and import the Table
Create a new project in Noodl using the Hello World template. Create a new Noodl Cloud Service by following this **[guide](/docs/guides/cloud-data/creating-a-backend)**. Once you have the backend, create a new class called Parks, and add the following columns (name, type):
- Image_src, String
- Name, String
- State, String
- Desc, String
- Size_km2, Number
- Established, Date
Then follow the **[Importing and exporting data](/docs/guides/cloud-data/import-export-csv)** guide to import the data from the following [csv-file](/docs/guides/visualizing-data/table-to-visualize-data/parks.csv).
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/6eZ1Zgo3qko" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe>
Select the Start Page component and remove the text node. Then on the **Page** node, add 80px padding all around.
<div className="ndl-image-with-background s">
![](/docs/guides/visualizing-data/table-to-visualize-data/page-padding.png)
</div>
Add a **Group** as a child to the **Page** and set the **Group's** **Dimension** property to be 100% width and the children's height. You can also enable scroll on this **Group**. This ensures that you can scroll the **Table** horizontally if it is large.
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/table-to-visualize-data/group-dimensions.png)
</div>
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-to-visualize-data/group-scroll.png)
</div>
Let's also add a **Text** node like this:
<div className="ndl-image-with-background xl">
![](/docs/guides/visualizing-data/table-to-visualize-data/add-text-node.png)
</div>
Style the **Text** node like this and have it say Parks in America:
<div className="ndl-image-with-background s">
![](/docs/guides/visualizing-data/table-to-visualize-data/text-style-1.png)
</div>
<div className="ndl-image-with-background s">
![](/docs/guides/visualizing-data/table-to-visualize-data/text-style-2.png)
</div>
Next, in the Start Page, bring up the Node Picker and select the Prefabs tab. Find the **Table** prefab, and click Clone.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-to-visualize-data/prefabs-tab.png)
</div>
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-to-visualize-data/clone-table.png)
</div>
Now you should see that you have a **Table** component in your Components View.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-to-visualize-data/table-in-comps.png)
</div>
Add a **Table** node to the node tree, by either dragging it in from the components view or by using the Node Picker. Put the **Table** under the **Text** node. Your node graph should look like this:
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-to-visualize-data/table-in-graph.png)
</div>
## Adding data to the Table
Now that we have a **Table** let's feed it with data from Parks class. Add a **[Query Records](/nodes/data/cloud-data/query-records)** node to your node graph and tell it to look at the Parks class.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-to-visualize-data/add-query.png)
</div>
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-to-visualize-data/query-details.png)
</div>
Next connect the <span class="ndl-data">Items</span> output from the **Query Records** node to the <span class="ndl-data">Items</span> input of the **Table**.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-to-visualize-data/items-connected.png)
</div>
Reload the project. Your table should now look like the image below:
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-to-visualize-data/table-with-data.png)
</div>
You might have noticed that when you reload the project, the **Table** shows some default data before the **Query Records** node has retrieved our parks data. To remove that default data, select the **Table** and click the Edit button for the Items and remove the objects in the array, so that you are left with an empty array like this:
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-to-visualize-data/items-empty-array.png)
</div>
Now if you reload your project the **Table** will be empty until it gets data from the **Query Records** node.
## Customizing the Header titles and what data to show
By default the **Table** node will display all the data fields it gets from the **Query Records**, and will use the same title for the header as the column name in the class.
By editing the **Headers** property we can control which fileds that are shown, what order the fields are shown, and we can change the titles to something more friendly than the default column names.
Click the Edit button next to the **Headers** property and fill in the following. You can refer to the **[Table](/library/prefabs/table)** documentation for a deeper explanation of how the Headers work.
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-to-visualize-data/edit-headers.png)
</div>
Then add the following code to the Headers section:
```javascript
[
{
Field: 'Image_src',
Label: 'Image',
Type: 'Image',
},
{
Field: 'Name',
Label: 'Name',
},
{
Field: 'State',
Label: 'State',
},
{
Field: 'Established',
Label: 'Established',
},
{
Field: 'Size_km2',
Label: 'Size',
},
{
Field: 'Desc',
Label: 'Description',
},
];
```
Here is a screenshot of the final result:
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-to-visualize-data/headers-code.png)
</div>
<div className="ndl-image-with-background l">
![](/docs/guides/visualizing-data/table-to-visualize-data/final-result.png)
</div>
## Summary
In this guide we used a **[Table](/library/prefabs/table)** node to display data from a **[Query Records](/nodes/data/cloud-data/query-records)** node and we used the **Headers** property on the **Table** node to control what data to show and in what order.

15
docs/learn.mdx Normal file
View File

@@ -0,0 +1,15 @@
---
title: Learn
hide_title: true
---
import { GuideListing } from '../src/blocks/GuideListing.tsx'
import { VideoListing } from '../src/blocks/VideoListing.tsx'
# Happy to see you here!
Noodl is a low code web app builder that will have you creating applications faster and smarter. It's a visual development environment that you don't need any previous coding skills to start learning. It's also great for developers who already know how to code as you can easily mix in JavaScript when appropriate.
<GuideListing title="All guides" hasNoLink />
<VideoListing title="All videos" hasNoLink />

241
docusaurus.config.js Normal file
View File

@@ -0,0 +1,241 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
//const lightCodeTheme = require('prism-react-renderer/themes/github');
//const darkCodeTheme = require('prism-react-renderer/themes/dracula');
const {
_parseNoodlMarkupPlugin,
_parseRenderedMarkupPlugin,
} = require('./plugins/markdown-syntax');
const version = require('./package.json')
.version.split('.')
.slice(0, 2)
.join('.');
// Reverse the sidebar items ordering (including nested category items)
function reverseSidebarItems(items) {
// Reverse items in categories
const result = items.map((item) => {
if (item.type === 'category') {
return { ...item, items: reverseSidebarItems(item.items) };
}
return item;
});
// Reverse items at current level
result.reverse();
return result;
}
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'Noodl',
tagline: 'Dinosaurs are cool',
url: 'https://docs.noodl.net',
baseUrl: `/${version}/`,
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
favicon: 'img/favicon.ico',
organizationName: 'Noodl', // Usually your GitHub org/user name.
projectName: 'Noodl', // Usually your repo name.
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
sidebarPath: require.resolve('./sidebars.js'),
breadcrumbs: false,
},
theme: {
customCss: [
require.resolve('./src/css/custom.css'),
require.resolve('./src/css/variables.css'),
require.resolve('./src/css/navbar.scss'),
require.resolve('./src/css/sidebar.scss'),
require.resolve('./src/css/searchbar.scss'),
require.resolve('./src/css/markdown.scss'),
require.resolve('./src/css/pagination.scss'),
],
},
}),
],
],
plugins: [
'docusaurus-plugin-sass',
// Node reference docs
[
'@docusaurus/plugin-content-docs',
{
id: 'nodes',
path: 'nodes',
routeBasePath: 'nodes',
sidebarPath: require.resolve('./sidebarsNodes.js'),
breadcrumbs: false,
remarkPlugins: [
_parseNoodlMarkupPlugin,
require('./plugins/import-markdown'),
],
rehypePlugins: [_parseRenderedMarkupPlugin],
},
],
// Javascript API
[
'@docusaurus/plugin-content-docs',
{
id: 'javascript',
path: 'javascript',
breadcrumbs: false,
routeBasePath: 'javascript',
sidebarPath: require.resolve('./sidebarsJavascript.js'),
},
],
// Library
[
'@docusaurus/plugin-content-docs',
{
id: 'library',
path: 'library',
routeBasePath: 'library',
breadcrumbs: false,
sidebarPath: require.resolve('./sidebarsLibrary.js'),
remarkPlugins: [
_parseNoodlMarkupPlugin,
require('./plugins/import-markdown'),
],
},
],
// CLI
[
'@docusaurus/plugin-content-docs',
{
id: 'cli',
path: 'cli',
routeBasePath: 'cli',
breadcrumbs: false,
sidebarPath: require.resolve('./sidebarsCli.js'),
remarkPlugins: [
_parseNoodlMarkupPlugin,
require('./plugins/import-markdown'),
],
},
],
// Whats new
[
'@docusaurus/plugin-content-blog',
{
id: 'whats-new',
path: 'whats-new',
routeBasePath: 'whats-new',
blogSidebarTitle: 'Recent updates',
postsPerPage: 1,
feedOptions: { type: 'json' },
showReadingTime: false,
remarkPlugins: [
_parseNoodlMarkupPlugin,
require('./plugins/import-markdown'),
],
},
],
// Copy static md files for editor inline docs
[
require('./plugins/copy-node-markdowns'),
{
paths: ['nodes/', 'library/modules/'],
},
],
[
require('./plugins/gtm'),
{
id: 'GTM-P4LSJL4',
},
],
],
themeConfig: {
metadata: [
{
property: 'og:image',
content: 'https://docs.noodl.net/noodl-docs.png',
},
{
property: 'og:title',
content: 'Noodl Documentation',
},
{
property: 'og:description',
content:
'Explore Noodl guides, tutorials, videos, modules, and reference documentation here. Noodl is the low-code platform for designers + developers to build custom web apps and experiences.',
},
{
name: 'google-site-verification',
content: 'KjANYZkN8ymGFD0SGnAVlRkD85p-fpNHCRAOKxurpWI'
}
],
algolia: {
appId: 'D29X2LNM4J',
apiKey: '7984d5feef068e1161527316bb9a1a4d',
indexName: 'docs_2-9',
},
colorMode: {
disableSwitch: true,
defaultMode: 'dark',
},
navbar: {
title: 'Docs',
logo: {
alt: 'Noodl Logo',
src: 'img/logo.svg',
},
items: [
{
label: 'Learn',
to: '/docs/learn',
position: 'right',
},
{
label: 'Node reference',
to: '/nodes/overview',
position: 'right',
},
{
label: 'Library',
to: '/library/overview',
position: 'right',
},
{
label: 'Javascript',
to: '/javascript/overview',
position: 'right',
className: 'has-divider',
},
{
label: 'Discord',
to: 'https://discord.com/invite/23xU2hYrSJ',
target: '_blank',
position: 'right',
className: 'is-discord',
},
{
label: 'Download',
to: 'https://console.noodl.net/#/signup',
target: '_blank',
position: 'right',
className: 'is-download-button',
},
],
},
},
};
module.exports = config;

View File

@@ -0,0 +1,33 @@
---
title: Change nodes at build time
hide_title: true
---
<head>
<meta name="robots" content="noindex,nofollow,noarchive" />
</head>
# Change nodes at build time
:::note
This is recommended to only use in 2.7.x.
If using it in 2.6.x, you will change the current project
which will not be temporary during building.
:::
```js
module.exports = {
async onPreBuild(context) {
// Get all the "Function" nodes
const functionNodes = context.project.getNodesWithType('JavaScriptFunction');
functionNodes.forEach((node) => {
// Replace all "Hello World" to "Hello" in the scripts
node.parameters.functionScript = node.parameters.functionScript
.replace("Hello World", "Hello");
});
},
};
```

View File

@@ -0,0 +1,147 @@
---
title: Create a build script
hide_title: true
---
<head>
<meta name="robots" content="noindex,nofollow,noarchive" />
</head>
# Build scripts
Noodl has a way where you can hook into the different build events that
are triggered from the editor.
:::danger
This is an experimental feature, that might be changed in the future.
:::
### Where to use it?
- [Generate a Sitemap and static pages](/javascript/extending/build-script/sitemap-and-seo)
- [Change nodes at build time](/javascript/extending/build-script/change-nodes-at-build-time)
## Create a build script
To add a build script it has to be placed inside a folder in the project.
As long as the file ends with `.build.js` it will be picked up by Noodl.
The execution order of the build scripts are based on alphabetical order.
```
my-noodl-project/
.noodl/
build-scripts/
[HERE]
```
### Example
Here is an example of what kind of events you can listen to:
```js
module.exports = {
async onPreBuild(context) {
// Occurs before the build.
},
async onPostBuild(context) {
// Occurs after the build.
},
async onPreDeploy(context) {
// Occurs before the build is deployed.
},
async onPostDeploy(context) {
// Occurs after the build is deployed.
},
};
```
## What is Context?
Context is a little different in each method,
but generally have the same methods.
:::note
More documentation to come later!
:::
### General
```ts
workspaceId: string;
project: ProjectModel;
environment: {
name: string;
description: string;
masterKey: string;
appId: string;
} | undefined;
/**
*
* @param options
* @param callback
*/
activity(options: { message: string; successMessage?: string; }, callback: () => Promise<void>): Promise<void>;
/**
*
* @param type
* @param message
*/
notify(type: 'notice' | 'success' | 'error', message: string): void;
/**
* Returns a list of all the pages with absolute paths.
*
* @returns [
* {
* title: "page title",
* path: "/path-1/path-2",
* meta: {}
* },
* // ...
* ]
*/
getPages(options: {
expandPaths?: (route: RouteNode) => Promise<string[]>;
}): Promise<readonly PageObject[]>;
/**
* Create a index.html page similar to the one created for the web app.
*
* @returns a string containg the HTML code.
*/
createIndexPage(options: {
/**
* Override the title from project settings.
*
* Default: undefined
*/
title?: string;
/**
* Append the headcode from the project settings.
*
* Default: undefined
*/
headCode?: string;
}): Promise<string>;
/**
* Returns a traversal graph of the nodes.
*
* @param selector
* @returns
*/
graphTraverse(
selector: (node: NodeGraphModel.Node) => boolean,
options: NodeGraphTraverserOptions
): NodeGraphTraverser;
```

View File

@@ -0,0 +1,196 @@
---
title: Build script that generate Sitemap and static pages
hide_title: true
---
<head>
<meta name="robots" content="noindex,nofollow,noarchive" />
</head>
# Generate a Sitemap and static pages
Having a `sitemap.xml` is very important for SEO,
so that search engines can build up a good map on the website.
With Noodl we can create build scripts that will alter the files after building.
What is really cool with this feature is that you can find all the information on
how the project is built, so we can find all the Pages nodes and generate a nice
sitemap and even create static index.html files to improve the SEO even more!
## Examples
Here are 2 examples of a script that generates a sitemap from all the pages.
### Static pages for Sitemap.xml
In case you don't have any dynamic pages, this script is really easy to use.
```js
const fs = require("fs");
/**
* Sitemap class that helps us build the sitemap XML.
*/
class Sitemap {
constructor() {
this.urls = [];
}
add(entry) {
this.urls.push(entry);
}
toXml() {
let xml = `<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"
>`;
xml += this.urls
.map((item) => {
let entry = `<url>`;
Object.keys(item).forEach((key) => {
entry += `<${key}>${item[key]}</${key}>`;
});
return entry + `</url>`;
})
.join("");
return xml + `</urlset>`;
}
}
module.exports = {
async onPostBuild(context) {
/**
* this.getPages() returns a flat array of all the pages,
* like this:
*
* [
* {
* title: "page title",
* path: "/path-1/path-2",
* }
* ]
*
*/
const pages = await context.getPages();
// Create our Sitemap class
const sitemap = new Sitemap();
// Loop over all the pages
pages.forEach((page) => {
// Add the page to the sitemap with
// the sitemap tags we want to use.
//
// Here is what kind of tags there are:
// https://www.sitemaps.org/protocol.html
sitemap.add({
// NOTE: In this example the path is not an absolute URL, which is required to make sitemaps work correctly.
loc: page.path,
changefreq: "weekly",
priority: 0.5,
});
});
// Write our sitemap.xml to the outputPath
//
// if you are deploying via our cloud service
// this will also upload the file.
await fs.promises.writeFile(
`${context.outputPath}/sitemap.xml`,
sitemap.toXml()
);
},
};
```
### Dynamic pages for Sitemap.xml
To make this one work you have to provide the information required by Noodl
to understand what your dynamic pages have.
```js
// We can do that by sending more information to the "getPages" method.
const pages = await context.getPages({
// Async function that is called for each page that has variables.
async expandPaths(route) {
// Check the current page path.
if (route.current.path === "page-3/{id}") {
// Return a list of our expanded paths.
return [
{
title: "My custom title",
path: "page-3/0",
meta: {
description: "My description",
keywords: "",
},
},
{
path: "page-3/1",
},
{
path: "page-3/2",
},
];
}
// Return the default value.
// this will not be called unless there is a variable in the path.
// So we are returning an invalid path here.
return [
{
path: route.current.path,
},
];
},
});
// ...
// To access the meta data from the pages we can call
// pages[0].meta
```
### Generate static pages
We can expand on the previous example with dynamic pages for Sitemap.xml.
In this case we want to take all the information and write a index.html file
for each page.
```js
const fs = require('fs');
const path = require('path');
// ...
// Loop over all the pages
for (let index = 0; index < pages.length; index++) {
const page = pages[index];
// Create the filepath where we want to save the new index.html file.
const pageDir = path.join(context.outputPath, page.path);
// Generate a index.html file with our custom title and headcode.
const content = await context.createIndexPage({
title: page.title,
headCode: `
<meta name="description" content="${page.meta.description}">
<meta name="keywords" content="${page.meta.keywords}">
`,
});
// Create all the folders to the path.
await fs.promises.mkdir(pageDir, {
recursive: true,
});
// Save our new index.html file.
await fs.promises.writeFile(path.join(pageDir, "index.html"), content);
}
```

View File

@@ -0,0 +1,161 @@
---
hide_title: true
hide_table_of_contents: true
title: Create a new core node
---
# Create a new core node
Noodl is very extensible. As a developer you can add new modules with new capablities, create connections to data or make new visual components in your workspace. This guide gets us started by showing how the Noodl command line tool works and how to create an extension module with a single new node. This node will behave and appear just like the standard core nodes of Noodl.
:::note
This guide requires <a href="https://nodejs.org/en/download/" target="_blank">Node.js</a> and <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm" target="_blank">npm</a> installed.
:::
## Overview
This guide will walk you through how to create a **Noodl Module**. A Noodl Module can contain new core nodes to use in your projects. You can for example wrap an existing JavaScript library and expose it as a node in Noodl.
The general process works like this
* Set up a new **Module Project** where you can write the code for your module.
* Test your module in one of you projects while developing it.
* When you are done, build the module and deploy it to the projects you want to use it in.
## Install the Noodl CLI
First you need to install the Noodl command line interfaces. If you have not previously installed the CLI you can do so via npm.
```bash
npm install -g @noodl/noodl-cli
```
## Create a Module Project
With the CLI tool you can easily create a new Noodl module from a template:
```bash
noodl-cli new lib ./my-noodl-module
```
You need to specify a directory name that will be created. The directory will contain everything you need to get started. Using the command above, the directory _my-noodl-module_ will be created.
The newly created directory has the following structure:
```
my-noodl-module/
module/
project/
tests/
icon.png
module.json
```
First some notes on the content of a library module:
- The **module** directory contains the source code for the module as well as build scripts and any assets you might want, such as fonts, css etc.
- The **project** and **tests** folder can be ignored
First enter the **module** directory and install the needed dependencies:
```bash
cd module
```
```bash
npm install
```
If your module uses any other external libraries through NPM they will be installed as well.
## Developing your module
You develop your module mainly by editing the file ``module/src/index.js``. From start it contains some example code that you can use as a boiler plate. There is currently no official documenation of the Noodl module SDK but you can find the source code to a number of modules [here](https://github.com/noodlapp).
As you are developing your module you would want it packaged up and deployed in a Noodl project where you can test it. To do that you first have to create a new Noodl project that will be your test project. Once you've done that, find the local folder of that project by clickin the cogwheel ("Settings") and "Open project folder".
<div class="ndl-image-with-background m">
![](/javascript/extending/open-project-folder.png)
</div>
Copy the full path to that folder - you will need it in the next step.
Now open the file ``/module/src/webpack.config.js``. Among other things, this file specifies where to deploy the module. We want to make sure its deployed to our test project.
Update the row containing ``var outputPath = ...`` to the following
```javascript
var outputPath = path.resolve('<the absolute path that your project>', 'noodl_modules/' + pjson.name);
```
Now go back to your terminal window (that was located in the ``modules/`` folder) and write the following.
```bash
npm run dev
```
This will enter development mode where your module is automatically rebuilt and redeployed to your project when you make changes in the source code.
If you started from the boiler plate code in ``module/src/index.js`` you will already have a module now in your project. Reload the Noodl project by closing it and opening it again, or simply press ctrl+R (Windows) / cmd+R (Mac) when you are in the Node Editor. Then bring up the Node Picker and you should see your new core node under "External Libraries".
## Overview of the module code
The file _index.js_ contains the code for your nodes. Open it in your favorite editor and have a look. The file contains boilerplate code for a simple new core node, let's look at the different sections:
First you must import the Noodl SDK.
```javascript
const Noodl = require('@noodl/noodl-sdk');
```
Next you will define the code for the new node.
```javascript
const MyFullNameNode = Noodl.defineNode({
category: 'My Utils',
name: 'Full Name',
inputs: {
FirstName: 'string',
LastName: 'string',
},
outputs: {
FullName: 'string',
},
changed: {
FirstName: function () {
this.setOutputs({
FullName: this.inputs.FirstName + ' ' + this.inputs.LastName,
});
},
LastName: function () {
this.setOutputs({
FullName: this.inputs.FirstName + ' ' + this.inputs.LastName,
});
},
},
});
```
- You need to specify the **name** of the node, this is the name that shows up in the list when creating new nodes.
- you can optionally specify a **category**, this will also be used in the new node popup in Noodl.
Finally you need to define the specification of your module.
```javascript
Noodl.defineModule({
nodes: [MyFullNameNode],
setup() {
//this is called once on startup
},
});
```
Again, check out the [Noodl Repo](https://github.com/noodlapp) at GitHub for some module examples.
## Deploying your module
When you are happy with your module you can do a proper deploy. Go back to the terminal window (still in the ``modules/`` folder) and write.
```bash
npm run build
```
This deploys an optimized version of the module. If you want to use the module in a different project, just change the path in ``/module/src/webpack.config.js`` and do ```npm run build``` again.

Some files were not shown because too many files have changed in this diff Show More