The learning content is split across several units. Each of them is focused on a key concept or functionality for the app. In this unit you will learn the fundamentals for oneJS programming. If you are an advanced JavaScript developer and you are familiar with Webpack and Expo, you can skip this unit.
Functions
A JavaScript function is a block of code designed to perform a particular task. Function parameters are listed inside the parentheses () in the function definition. In JavaScript there are two ways of defining functions:
Function Keyword
functionadd(x,y) = {returnx + y;}
Arrow Function
constadd = (x,y)=>{returnx + y;}
The arrow function is the preferred notation as it more compact in complex scenarios. When a JavaScript function reaches a return statement, the function will stop executing.
Functional Programming
How are functions related to functional programming? FP is a programming paradigm where programs are constructed by applying and composing functions. In functional programming, functions are treated as first-class citizens, meaning that they can be bound to names, passed as arguments, and returned from other functions, just as any other data type can.
Functional programming is sometimes treated as synonymous with purely functional programming, a subset of functional programming which treats all functions as deterministic mathematical functions, or pure functions.
Pure Functions
Pure functions are a subset of functions that possess a set of unique properties:
Referential Transparency
Pure functions always produce the same output for same arguments irrespective of anything else.
This is true for oneJS. Components are defined as functions and if they are called with the same parameters (i.e. component properties), they will look and behave the same way.
No Side Effects
Pure functions do not modify any arguments, local/global variables or input/output streams.
This is only partially true for oneJS. An app composed uniquely of strictly pure functions would be completely static as no side effects means no changes in the app’s state. In oneJS, we follow what we call the No Implicit Side Effect philosophy. Functions will never modify external state implicitly in their definition. This would break function’s isolation, as the content of the function would need to be aware of its exterior. In oneJS, a function can only modify the state of the application if it is explicitly forced to do so when it is called. You will find how to do so in Unit 2: State.
Why Functional Programming?
Modular
Since every function only depends on its input arguments, they enable loosely coupled architectures like modular design. This increases increases overall productivity. Small modules can be coded quickly and have a greater chance of being re-usable. Moreover, modules can be tested separately which helps you reduce the time spent on unit testing and debugging.
Maintainability
Functional programming is easier to maintain as you don’t need to worry about accidentally changing anything outside the given function.
Project Structure
One of the advantages of oneJS is being able to use a single codebase for web and native apps. The first step is to set up your development environment using the app creator as indicated in the get started page. This creates a template project with the predefined structure described below:
assets: All the non-source-code resources required by your app. These can include fonts, vector icons, logo images, texts, etc.
public: Contains the index.html file that will render your web app.
src: The source code describing your app. The main file is App.js.
.babelrc: Enables importing .svg files directly into your source code.
babel.config.js: contains the Babel preset configuration for Expo.
package.json: Describes your npm package. Defines the dependencies you need to install and the scripts you can run to build and serve your app.
webpack.config.js: The configuration file for Webpack module bundler. Web only.
app.json: It is used for configuring how a project loads in Expo. You can think of this as an index.html but for React Native apps. iOS/Android only.
By default, oneJS uses bundling tools: A bundler is a tool that packages your code and all its dependencies together in a single JavaScript file.
There are several bundlers you can choose from. Out of the box, oneJS uses Webpack for the web and Expo for iOS and Android. It is not the scope of this website to describe in detail the functionality of these products, but if you need more information about the tools that power oneJS, make sure to refer to the following links:
Now that you know what functions are, we are going to use them to create a simple app. There are three main pillars to any app: Structure, State and Style. In this unit you will learn all about structure, i.e., the fundamentals to render components in your app.
Components in oneJS are functions that take properties and return the structure to be rendered. The structure represents the visible and interactable portion of your app. Given the same properties they will always return a component that looks and behaves in the same way.
To distinguish component functions from other functions their name always starts with a capital letter. Let’s take a look at the two types of components:
Regular Components
They take properties and return a component. They do not require any additional structure. E.g.: inputs or icons.
<!–– HTML Definition ––>
<inputtype="text"id="myInput"style="color: red"/>
They take properties as well as other structure as inputs to return a component. E.g views or texts.
Wrapper components are curried functions. What this means in practice is that it takes two sets of parameters: first for the properties and second for the structure. This is just for convenience; this separation of concerns makes the code look cleaner and it’s easier to nest structures.
<!–– HTML Definition ––>
<divid="myView"style="background-color: blue"/><pclass="text">Hello World!</p></div>
You may note that in most cases when we display code, we do not specify the target platform; web vs. iOS/Android. This is because the same code is valid for both. If we do not explicitly indicate that a difference exists, you can rightfully assume that the code presented is cross-platform.
App Function
In the previous examples, we have explored the nomenclature to create components, but these components will not be visible unless we render them on screen. The way to instruct oneJS to render our components is through the app function. It takes the following properties and renders the output on screen:
state: The state variables governing your app’s behavior. Learn all about it in Unit 2.
/* Configure App State */conststate = {stateVariable1:'defaultValue1',stateVariable2:'defaultValue2'}app({component:App,state:state});
theme: The theming options chosen to create a homogeneous user experience for your app. You can use oneJS’ preset theme by providing the theme property the value below: Learn all about it in Unit 3.
style: The JavaScript object describing your app’s styling. You can use it in your components by calling the readStyle function with the corresponding style id. Learn all about it in Unit 3.
font: You can optionally provide custom fonts to be loaded and used in your app:
/* Import Font */constfont = {'Avenir Next':require('../assets/fonts/AvenirNext.otf'),'Avenir Next Bold':require('../assets/fonts/AvenirNextBold.otf')};app({component:App,font:font});
firestore: You can optionally provide a configured Firestore object to synchronize the app’s state with your database. Learn all about it in Unit 8.
/* Configure Firestore */constfirebaseConfig = {apiKey:"API_KEY",authDomain:"PROJECT_id.firebaseapp.com",// The value of "databaseURL" depends on the location of the databasedatabaseURL:"https://DATABASE_NAME.firebaseio.com",projectId:"PROJECT_id",storageBucket:"PROJECT_id.appspot.com",messagingSenderId:"SENDER_id",appId:"APP_id",// For Firebase JavaScript SDK v7.20.0 and later, "measurementId" is an optional fieldmeasurementId:"G-MEASUREMENT_id",};constfirebaseApp = initializeApp(firebaseConfig);constfirestoreDB = getFirestore();app({component:App,firestore:firestoreDB});
component: The component function encapsulating your entire app. For clarity, we name this component App in the examples but you can give it any name you want to. This component function is a bit special because it takes no properties and just returns the desired structure:
/* Imports */import{app}from'@onejs-dev/core';import{View,Text,Input}from'@onejs-dev/components';/* App Component: Takes no properties and returns structure */constApp = ()=>{returnView({id:'myView'})([Text({style:{fontSize:20}})('Name: '),Input({type:'text',style:{background:'pink'}})]);};/* App Function: Renders the App Component in the screen */app({component:App,theme:{preset:'oneJS'}});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
Native Components
oneJS puts the complete set of standard HTML and React Native components at your disposal. Just import them from the components module and use them as you would normally.
/* Imports */import{app}from'@onejs-dev/core';import{HtmlDiv,HtmlP,HtmlInput}from'@onejs-dev/components';/* App Component: Takes no properties and returns structure */constApp = ()=>{returnHtmlDiv({id:'myView'})([HtmlP({style:{fontSize:20}})('Name: '),HtmlInput({type:'text',style:{background:'pink'}})]);};/* App Function: Renders the App Component in the screen */app({component:App});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
Behind the scenes, we wrap the standard components with a Higher Order Component. This allows to provide some extra functionality.
Lifecycle Events (Web only)
Lifecycle events properties allow you to provide functions that are called when these events are triggered. The function provided can contain a component property that you can use to attach listeners or perform other transformations to it.
onInit: It is the first function being triggered. It is called before the component is attached to the DOM (Document Object Model). Note that this the only function that does not provide a component property.
onCreate: It is triggered when the component is attached to the DOM. This is where you should attach your listeners. It is equivalent to React’s useEffect(function, []) hook.
onPropertyChange: It is triggered when a state property is modified. It takes an object whose keys are the property ids and values are the event handling functions. The handling function takes the new value and component as arguments.
/* Imports */import{app,read,update}from'@onejs-dev/core';import{View,Text,Input}from'@onejs-dev/components';/* State Configuration */conststate = {inputValue:'pink'};/* Property Change Function */constchangeAction = (newValue,component)=>{component.style.backgroundColor = newValue;}/* App Component: Takes no properties and returns structure */constApp = ()=>{returnInput({type:'text',value:read('inputValue'),onChange:update('inputValue'),onPropertyChange:{value:changeAction}});};/* App Function: Renders the App Component in the screen */app({component:App,state:state,theme:{preset:'oneJS'}});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
Style
Style is one of the fundamentals for any app; the entire Unit 3 is dedicated to it.
On iOS and Android the style property is directly mapped to React Native elements' style property.
On the web, you can pass any JavaScript object supported by Emotion to your components' style property. oneJS will compile it to generate a class name that is applied to your component. This is different from React’s style property that applies the CSS inline, with oneJS you will be able to leverage the full power of CSS in JS.
This should be your last option, but if for any reason you are forced to use styles inline you can do so by feeding the inlineStyle property (web only):
This is a convenient property to make your components clickable redirecting the user to the url of your choice. This will be one of the key features for routing in Unit 6.
So far, we have only talked about standard components but oneJS provides its own set of additional components that aim to solve some of the grievances programmers face. We have also made these components cross platform so that if you are on the web they are transformed into HTML and if you are using iOS or Android, they are transformed into React Native components.
This is a summary of the out of the box cross-platform components. You can compose almost any interface with a combination of these:
Text: Your everyday text component. It is a best practice to wrap all your texts with this component instead of directly writing inside of view boxes.
Input: Consolidates all the different types of inputs, including textarea and select inputs.
View: The most powerful oneJS component; you can position, navigate and animate your content with a single component.
Icon: Staple component for vector svg icons. You can effortlessly use gradients and work with other fonts.
Modal: Context window that becomes visible on user actions.
Learn the specifics for these components in the Docs section.
Unit 2: State
An app is a living entity, it is updated with every interaction from the user or the database. If we would take a snapshot of the app, the state describes the value for that moment in time of all the parameters that govern its behavior.
Thus, the state can be regarded as the collection of variables that govern the behavior and appearance of the app.
Let’s begin with a simple example; an app with a text input that we will use to update the name of the user. In this case, userName is the only state variable required.
/* Imports */import{app,read,update}from'@onejs-dev/core';import{View,Text,Input}from'@onejs-dev/components';/* App Component: Takes no properties and returns structure */constApp = ()=>{returnView({content:{direction:'column'}})([Text()('User Name: '),Input({type:'text',placeholder:'Name'})]);};/* App Function: Renders the App Component in the screen */app({component:App,theme:{preset:'oneJS'}});
Declaration
The first step is to declare all the state variables of the app as a configuration object. The object takes as keys the state variable ids. The value, is an object that can take the following properties:
default
The default value for the state variable. If no other value is retrieved or set, this will be its initial value. The default value can be provided as an object property or as a string:
/* State Configuration */conststate = {userName:{default:'my name'}};
/* State Configuration */conststate = {userName:'my name'};
source and storage
source, represent the origin for the value of the state variable. storage is the location where the state variable’s value is saved. They can take the following values:
local: For source, the value is retrieved from the selected path in local storage. For storage, the value is saved in the selected path in the user’s local storage.
/* State Configuration */conststate = {userName:{source:{local:'userName'},storage:{local:'userName'}}};
firestore: For source, the value is retrieved from the selected path in the Cloud Firestore database. For storage, the value is saved in the selected path in the Cloud Firestore database. You can use this configuration to sync your only database with the app. Learn all about source and storage in Unit 8.
/* State Configuration */conststate = {userData:{source:{firestore:'users/id1234'},storage:{firestore:'users/id1234'}}};
url: Retrieves the value from the selected path segment of the actual url if it matches the selected url. Otherwise, returns undefined. The value to be fetched is represented by :. You can use * to match any value for a certain path segment. url can only be used as source for the state variables.
Once the configuration object is created, feed this value to the app function’s state property.
/* State Configuration */conststate = {userName:{default:'my name'}};app({component:App,state:state});
Read
Once the state is defined, we need a means of accessing it. oneJS provides the read function that returns the state variable’s current value for the selected state id; in this case userName.
Now in the example we can check how the default value of the state variable is applied to the input, but we still need a way for the input to update its value. Learn about it in the following section.
/* Imports */import{app,read}from'@onejs-dev/core';import{View,Text,Input}from'@onejs-dev/components';/* State Configuration */conststate = {userName:''};/* App Component: Takes no properties and returns structure */constApp = ()=>{returnView({content:{direction:'column'}})([Text()('User Name: ' + read('userName')),Input({type:'text',placeholder:'Name',value:read('userName'),})]);};/* App Function: Renders the App Component in the screen */app({component:App,state:state,theme:{preset:'oneJS'}});
Update
In functional programming, functions should not directly update the state. This would break their isolation as they would need to “be aware” of the context where they are called in. On the other hand, an app unable to change its state would be unusable.
In oneJS, while functions will never directly access the state, we allow the user to bind to the component event functions such as the onChange event. You can pass a function to the onChange property that reads the event’s changed value and uses it to update the state. Exactly for this purpose, oneJS provides the update function; it takes as first parameter the name of the state variable to be updated and automatically updates the state when the onChange event is triggered.
/* Imports */import{app,read,update}from'@onejs-dev/core';import{View,Text,Input}from'@onejs-dev/components';/* State Configuration */conststate = {userName:''};/* App Component: Takes no properties and returns structure */constApp = ()=>{returnView({content:{direction:'column'}})([Text()('User Name: ' + read('userName')),Input({type:'text',placeholder:'Name',value:read('userName'),onChange:update('userName'),})]);};/* App Function: Renders the App Component in the screen */app({component:App,state:state,theme:{preset:'oneJS'}});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
Unit 3: Style
If the structure represents the skeleton, the style is the external appearance, the skin of the app. The same structure can look completely different in two apps that apply different styles to it.
In oneJS, you can style all your components using JavaScript. JavaScript style attributes match the CSS attributes on the web using camel case notation; e.g. border-radius is written as borderRadius. If you are already familiar with CSS, you can leverage all of its power to customize your app.
For the web, Emotion is used to read and compile style objects. For iOS and Android, oneJS uses the default React Native style property.
Style Property
To apply your style, you can use the style property in any component. The style property can receive a JavaScript object or an array of object styles. The priority increases with the position in the array; the last style has precedence.
On the web, using Emotion, styles are internally compiled into a class and applied to your component. This enables to use nested structures, media queries, animations and much more!
In general, it should be avoided but if there is no other remedy, you can use inline styles by feeding the inlineStyle property. This can only be done in web components as there is no such distinction between class and inline styles in React Native.
/* Imports */import{app}from'@onejs-dev/core';import{View,Input}from'@onejs-dev/components';/* Style */constbutton1Style = {color:'white',backgroundColor:'blue'};constbutton2Style = {backgroundColor:'red',borderRadius:30};/* App Component: Takes no properties and returns structure */constApp = ()=>{returnView()([Input({type:'button',title:'Button 1',style:button1Style}),Input({type:'button',title:'Button 2',style:[button1Style,button2Style]}),Input({type:'button',title:'Button 3',inlineStyle:{color:'green'}}),]);};/* App Function: Renders the App Component in the screen */app({component:App});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
As your app grows in complexity, it is often cleaner to wrap all your styles in single object. You can then feed this object to the app function's style property. This enables using the readStyle function to apply styles to your components:
For small apps it may be enough to apply styles directly to components. In larger apps, it is helpful to define a set of guidelines that govern the look and feel of components without having to specify their style every time they are used. oneJS provides scalable theming capabilities; we are confident that if you give them a try you are going to love them.
A theme, is the collection of all the variables that you would use to customize the look and feel of your app. Out of the box, oneJS provides the following selection of theme variables:
constthemeVariables = {/* The values below show the default values for oneJS preset theme *///ColorbackgroundColor:'#ffffff',//The color for the component's background.primaryColor:'#094DFF',//The primary contrast color for the component.neutralColor:'#b1b0c5',//Neutral color that can be used for backgrounds or disabled buttons.acceptColor:'#60b33a',//Color for affirmative actions. Usually green.rejectColor:'#ff5100',//Color for negative actions. Usually red.warnColor:'#f7df00',//Color for warning. Usually yellow.lightColor:'#e9e8ee',//Light color for icons or backgrounds.darkColor:'#4c4b66',//Dark color for icons or backgrounds//TexttextFont:'Avenir Next',//Font family for texts.textColor:'#666488',//Font color for texts.textSize:18,//Font size for texts.textWeight:'normal',//Font weight for texts.//Borderradius:10,//Border radius to create round corners.borderWidth:1,//Border width.borderStyle:'solid',//Border style.borderColor:'#e9e8ee',//Border color.//Shadowshadow:{elevation:0},//The component's elevation creating a shadow. Value from 0 to 20.};
These variables are then clustered into what we can flavors. A theme can have multiple flavors which in turn can have multiple variables. A flavor is a cluster of theme variables that possess a value differing from the default one. Out of the box, oneJS provides the following selection of flavors:
To use theme flavors in your app, you need to first provide a configuration object to the App component. This configuration object specifies the following parameters:
variables: All the theme variables and corresponding values. You can override one, several or all the preset variables that were shown on the example above. You can also choose to create a totally different variable collection that suits your app better.
flavors: All of the flavors bundling the theme variables with their corresponding values. You can override one, several or all the preset flavors that were shown on the example above.
Now we need a way of applying these flavors to components. All oneJS components contain the flavor property; use the readFlavor function with the flavor id to retrieve the corresponding theme variables.
/* Imports */import{app,readFlavor}from'@onejs-dev/core';import{View,Input}from'@onejs-dev/components';/* Theming */constflavors = {button1Flavor:{textColor:'white',radius:30,backgroundColor:'blue'}};/* App Component: Takes no properties and returns structure */constApp = ()=>{returnView()([Input({type:'button',title:'Button 1',flavor:readFlavor('button1Flavor')}),]);};/* App Function: Renders the App Component in the screen */app({component:App,theme:{flavors:flavors}});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
You may wonder, what can be accomplished with oneJS themes that you couldn’t with CSS classes. There is a critical difference, CSS classes talk the language of CSS properties while oneJS themes talk the language of designers and content creators and are coding language agnostic. Defining your own flavors is intuitive and you are not required to know anything about the underlying language.
See the oneJS preset flavors in action. Spice up your creation with different flavors!
/* Imports */import{app,readFlavor}from'@onejs-dev/core';import{View,Input}from'@onejs-dev/components';constpresetFlavors = [//Outline'default','shadow','noShadow','border','noBorder','flat',//Text'bold','normal','title','header','section','subsection','summary',//Selection'reverse','selected','unselected',//Colors'primary','primaryBackground','white','whiteBackground','light','lightBackground','neutral','neutralBackground','dark','darkBackground','accept','acceptBackground','reject','rejectBackground','warn','warnBackground'];/* App Component: Takes no properties and returns structure */constApp = ()=>{returnView({content:{wrap:true,gap:10}})([presetFlavors.map(flavor=>Input({type:'button',title:flavor,key:flavor,flavor:readFlavor(flavor)}))]);};/* App Function: Renders the App Component in the screen */app({component:App,theme:{preset:'oneJS'}});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
Gradients
One unique feature offered by oneJS is extensive support for linear gradients. You may have worked with gradients before, and there is a high change that you have found it quite cumbersome. We wanted to simplify this process by providing an intuitive and unified way of working with them. Gradients have many ways of being specified; CSS, SVG and React Native each follow their own convention. In oneJS we provide a wrapper for all of them following a concept you may already be familiar with, the unit circle.
To create a gradient you need to specify the following data; an angle in degrees and an array with two or more colors. The direction of the gradient is given by the angle θ in the unit circle. Colors go from origin (center of the circumference) to terminal point (tip of the vector) and are evenly distributed along the length of the vector. For example, a 45° linear gradient from purple to blue and from blue to red would look like the following images:
In oneJS, gradients are created as part of flavors. You can apply gradients to texts, backgrounds and icons by specifying the textGradient, backgroundGradient and primaryGradient theme variables respectively.
/* Imports */import{app,readFlavor}from'@onejs-dev/core';import{View,Text,Icon}from'@onejs-dev/components';import{icons}from'@onejs-dev/icons';/* Flavor Definition */constthemeFlavors = {myGradient:{textGradient:{angle:0,colors:['green','blue']},primaryGradient:{angle:45,colors:['pink','red']},backgroundGradient:{angle:90,colors:['silver','white']}}};/* App Component: Takes no properties and returns structure */constApp = ()=>{returnView({flavor:readFlavor('myGradient')})([Text({flavor:readFlavor('myGradient')})('Create beautiful gradients'),Icon({icon:icons['heart'],flavor:readFlavor('myGradient')})]);};/* App Function: Renders the App Component in the screen */app({component:App,theme:{flavors:themeFlavors}});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
To add the icing on the cake, oneJS automatically creates the best looking gradients for the colors you choose for your theme. For example, if you have defined a theme variable named secondaryColor oneJS automatically creates a flavor named secondaryColorGradient that you can apply to your components. See below the gradients for the preset theme variables:
/* Imports */import{app,readFlavor}from'@onejs-dev/core';import{View,Input}from'@onejs-dev/components';constpresetGradients = ['primaryGradient','whiteGradient','lightGradient','neutralGradient','darkGradient','acceptGradient','rejectGradient','warnGradient',];/* App Component: Takes no properties and returns structure */constApp = ()=>{returnView({content:{wrap:true,gap:10}})([presetGradients.map(flavor=>Input({type:'button',title:flavor,key:flavor,flavor:readFlavor(flavor)}))]);};/* App Function: Renders the App Component in the screen */app({component:App,theme:{preset:'oneJS'}});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
Congratulations! You have completed one of the most important units. Now you should be able to fully customize your app to your liking. Put all these new concepts to the test!
Unit 4: Positioning
You may have already noticed the View component in previous units’ examples. View is much more than a simple wrapper, it provides intuitive positioning and animation capabilities. This positioning is based on the CSS flexbox specification with some adaptations. Why flexbox instead of CSS grid you may ask? The main reason is that React Native does not yet offer support for grid layouts.
In oneJS we divide positioning into two types: content and self-positioning. content governs how children components are positioned, while self controls how the component itself it’s positioned with respect to its parent.
Content Positioning
To understand content positioning, you need to become familiar with the following concepts:
direction: The direction of the layout, i.e., how component’s children are organized. For row: Content flows along the x-axis (horizontal). column: Content flows along the y-axis (vertical). The default value is row.
wrap: If true, wraps the content adding new lines if it exceeds the space in the content’s direction. If false, it will not create more lines even if the space is exhausted. The default value is false.
h: Horizontal positioning of the component’s children. Possible values are: left, center, right, space, distribute and stretch. left, center and right are self-explanatory. space separates each of the children component as far as possible from each other. distribute creates equal spacing between them and the edges. stretch stretches children components across in the content’s transversal direction to fill the component; for the h property, it will only take effect if the direction is column.
v: Vertical positioning of the component’s children. Possible values are: top, center, bottom, space, distribute and stretch. top, center and bottom are self-explanatory. space separates each of the children component as far as possible from each other. distribute creates equal spacing between them and the edges. stretch stretches children components across in the content’s transversal direction to fill the component; for the v property, it will only take effect if the direction is row.
gap: Spacing between component’s children.
Put these properties to the test:
/* Imports */import{app,read,update,readFlavor,readStyle}from'@onejs-dev/core';import{View,Input,Text,Icon}from'@onejs-dev/components';import{icons}from'@onejs-dev/icons';/* State configuration */conststate = {direction:'row',wrap:true,h:'left',v:'top',gap:0}/* Input options */constoptions = {direction:['row','column','row-reverse','column-reverse'],wrap:[true,false],h:['left','center','right','space','distribute','stretch'],v:['top','center','bottom','space','distribute','stretch']}/* Style object creation */conststyle = {label:{width:85,fontWeight:'bold',textAlign:'right',paddingRight:10},childrenSize:{width:50,height:50},oneSize:{width:35,height:35},containerSize:{width:230,height:230},inputPanelSize:{width:430},inputSize:{minWidth:50,width:115}}/* App Component: Takes no properties and returns structure */constApp = ()=>{return[/* Content option panel*/Text({flavor:readFlavor('subsection')})('Content Options:'),View({flavor:readFlavor('outline'),content:{wrap:true},style:readStyle('inputPanelSize')})([Text({style:readStyle('label')})('direction'),Input({type:'list',options:options.direction,value:read('direction'),onChange:update('direction'),style:readStyle('inputSize')}),Text({style:readStyle('label')})('wrap'),Input({type:'list',options:options.wrap,value:read('wrap'),onChange:update('wrap'),style:readStyle('inputSize')}),Text({style:readStyle('label')})('h'),Input({type:'list',options:options.h,value:read('h'),onChange:update('h'),style:readStyle('inputSize')}),Text({style:readStyle('label')})('v'),Input({type:'list',options:options.v,value:read('v'),onChange:update('v'),style:readStyle('inputSize')}),Text({style:readStyle('label')})('gap: ' + read('gap')),Input({type:'range',min:0,max:20,value:read('gap'),onChange:update('gap'),style:readStyle('inputSize')})]),/* View parent component for "content" property*/View({flavor:readFlavor('lightBackground'),style:readStyle('containerSize'),content:{direction:read('direction'),wrap:read('wrap') !== 'false' ? true : false,h:read('h'),v:read('v'),gap:Number(read('gap'))},})([View({flavor:readFlavor('darkGradient'),style:readStyle('childrenSize')})(Text({flavor:readFlavor('section','light')})('0')),View({flavor:readFlavor('primaryGradient'),style:readStyle('oneSize')})(Text({flavor:readFlavor('section','light')})('1')),View({flavor:readFlavor('darkGradient'),style:readStyle('childrenSize')})(Text({flavor:readFlavor('section','light')})('2')),View({flavor:readFlavor('darkGradient'),style:readStyle('childrenSize')})(Text({flavor:readFlavor('section','light')})('3')),View({flavor:readFlavor('darkGradient'),style:readStyle('childrenSize')})(Text({flavor:readFlavor('section','light')})('4')),View({flavor:readFlavor('darkGradient'),style:readStyle('childrenSize')})(Text({flavor:readFlavor('section','light')})('5')),View({flavor:readFlavor('darkGradient'),style:readStyle('childrenSize')})(Text({flavor:readFlavor('section','light')})('6')),View({flavor:readFlavor('darkGradient'),style:readStyle('childrenSize')})(Text({flavor:readFlavor('section','light')})('7')),View({flavor:readFlavor('darkGradient'),style:readStyle('childrenSize')})(Text({flavor:readFlavor('section','light')})('8')),View({flavor:readFlavor('darkGradient'),style:readStyle('childrenSize')})(Text({flavor:readFlavor('section','light')})('9'))])];};/* App Function: Renders the App Component in the screen */app({component:App,state:state,style:style,theme:{preset:'oneJS'}});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
Self Positioning
For self positioning, the following concepts will allow you to have full control of your component’s positioning with respect to its parent.
align: Overrides the parent’s positioning in the content’s transversal direction. For example, if the parent instructs the content to flow in row direction, align will modify the vertical alignment. For row direction the possible values are: top, center, bottom, stretch and auto. For column direction the possible values are: left, center, right, stretch and auto.
expand: Capacity to grow if there is extra space. It is the equivalent to stretch but in the content’s direction. The greater the number provide to expand, the greater the expansion with respect to other sibling components. It is 0 by default, which means the component is unable to expand.
shrink: Capacity to shrink if there is not enough space. The greater the number, the greater the shrinkage with respect to other siblings. 0 means the component is unable to shrink. It is 1 by default.
/* Imports */import{app,read,update,readFlavor,readStyle}from'@onejs-dev/core';import{View,Input,Text,Icon}from'@onejs-dev/components';import{icons}from'@onejs-dev/icons';/* State configuration */conststate = {direction:'row',wrap:true,h:'left',v:'top',gap:0,align:'auto',expand:0,shrink:1}/* Input options */constoptions = {direction:['row','column','row-reverse','column-reverse'],wrap:[true,false],h:['left','center','right','space','distribute','stretch'],v:['top','center','bottom','space','distribute','stretch'],align:['auto','left','right','top','bottom','center','stretch']}/* Style object creation */conststyle = {label:{width:85,fontWeight:'bold',textAlign:'right',paddingRight:10},childrenSize:{width:50,height:50},oneSize:{width:35,height:35},containerSize:{width:230,height:230},inputPanelSize:{width:430},inputSize:{minWidth:50,width:115}}/* App Component: Takes no properties and returns structure */constApp = ()=>{return[/* Content option panel*/Text({flavor:readFlavor('subsection')})('Content Options:'),View({flavor:readFlavor('outline'),content:{wrap:true},style:readStyle('inputPanelSize')})([Text({style:readStyle('label')})('direction'),Input({type:'list',options:options.direction,value:read('direction'),onChange:update('direction'),style:readStyle('inputSize')}),Text({style:readStyle('label')})('wrap'),Input({type:'list',options:options.wrap,value:read('wrap'),onChange:update('wrap'),style:readStyle('inputSize')}),Text({style:readStyle('label')})('h'),Input({type:'list',options:options.h,value:read('h'),onChange:update('h'),style:readStyle('inputSize')}),Text({style:readStyle('label')})('v'),Input({type:'list',options:options.v,value:read('v'),onChange:update('v'),style:readStyle('inputSize')}),Text({style:readStyle('label')})('gap: ' + read('gap')),Input({type:'range',min:0,max:20,value:read('gap'),onChange:update('gap'),style:readStyle('inputSize')})]),/* Align option panel */Text({flavor:readFlavor('subsection')})('Align Options:'),View({flavor:readFlavor('outline'),content:{wrap:true},style:readStyle('inputPanelSize')})([Text({style:readStyle('label')})('expand: '+ read('expand')),Input({type:'range',min:0,max:1,value:read('expand'),onChange:update('expand'),style:readStyle('inputSize')}),Text({style:readStyle('label')})('shrink: ' + read('shrink')),Input({type:'range',min:0,max:1,value:read('shrink'),onChange:update('shrink'),style:readStyle('inputSize')}),Text({style:readStyle('label')})('align'),Input({type:'list',options:options.align,value:read('align'),onChange:update('align'),style:readStyle('inputSize')})]),/* View parent component for "content" property*/View({flavor:readFlavor('lightBackground'),style:readStyle('containerSize'),content:{direction:read('direction'),wrap:read('wrap') !== 'false' ? true : false,h:read('h'),v:read('v'),gap:Number(read('gap'))},})([View({flavor:readFlavor('darkGradient'),style:readStyle('childrenSize')})(Text({flavor:readFlavor('section','light')})('0')),/* View child component for "self" property */View({flavor:readFlavor('primaryGradient'),style:readStyle('oneSize'),self:{align:read('align'),expand:read('expand'),shrink:read('shrink')}})(Text({flavor:readFlavor('section','light')})('1')),View({flavor:readFlavor('darkGradient'),style:readStyle('childrenSize')})(Text({flavor:readFlavor('section','light')})('2')),View({flavor:readFlavor('darkGradient'),style:readStyle('childrenSize')})(Text({flavor:readFlavor('section','light')})('3')),View({flavor:readFlavor('darkGradient'),style:readStyle('childrenSize')})(Text({flavor:readFlavor('section','light')})('4')),View({flavor:readFlavor('darkGradient'),style:readStyle('childrenSize')})(Text({flavor:readFlavor('section','light')})('5')),View({flavor:readFlavor('darkGradient'),style:readStyle('childrenSize')})(Text({flavor:readFlavor('section','light')})('6')),View({flavor:readFlavor('darkGradient'),style:readStyle('childrenSize')})(Text({flavor:readFlavor('section','light')})('7')),View({flavor:readFlavor('darkGradient'),style:readStyle('childrenSize')})(Text({flavor:readFlavor('section','light')})('8')),View({flavor:readFlavor('darkGradient'),style:readStyle('childrenSize')})(Text({flavor:readFlavor('section','light')})('9'))])];};/* App Function: Renders the App Component in the screen */app({component:App,state:state,style:style,theme:{preset:'oneJS'}});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
Visibility
You can easily show and hide components by feeding the visible property. If true, the component will be visible on the app. If false, the visibility is set to none hiding it.
/* Imports */import{app,read,update,readFlavor}from'@onejs-dev/core';import{View,Input,Text}from'@onejs-dev/components';/* State Configuration */conststate = {isVisible:true};/* App Component: Takes no properties and returns structure */constApp = ()=>{returnView({content:{gap:10}})([Input({type:'switch',value:read('isVisible'),onChange:update('isVisible')}),View({visible:read('isVisible'),flavor:readFlavor('primaryGradient'),style:{padding:40}})(Text({flavor:readFlavor('light')})('Toggle visibility'))]);};/* App Function: Renders the App Component in the screen */app({component:App,state:state,theme:{preset:'oneJS'}});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
This completes your knowledge on how to position components on screen. In the next unit you will learn how to create your own custom components.
Unit 5: Custom Components
This may be one of the most important units. After completion, you will be able to create your own reusable components and share them with the rest of the world.
Throughout this unit, we will create a Password Validator component to illustrate the different steps to component creation. As input, you can select the checks to be performed to the password, such as: have at least one number, capital letter, symbol, etc. The validations are performed with regular expressions (regex):
constchecksConfig = {minCharacters:{regex:/.{8,}/,text:'Be at least 8 characters'},numbers:{regex:/\d+/,text:'At least one number'},letters:{regex:/[a-z]/,text:'At least one letter'},capitalLetters:{regex:/[A-Z]/,text:'At least one capital letter'},symbols:{regex:/[!-/:-@[-`{-~]/,text:'At least one special character'},...config}
After the validations, the component displays the requirement or checks that have not yet been met. See the expected final result below:
Definition
So far, in previous units we have showed you predefined components, but it is time to learn how to create your own. We have already established that components are functions. These functions take properties as arguments and return a component structure. Depending on the type of component you want to build, besides properties it can also require structure as input. In the example below, the Greeting custom component takes the name property and displays a greeting message to the selected name:
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
Depending on the type of component you want to build, besides properties it can also require structure as input. For convenience, instead of creating another property for the structure, we place this property as the input for a nested function. This leads to cleaner and more legible code when the component is instantiated. Take the ColorCard example below:
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
The component functions above are valid, but we can still optimize them. Use the Component function providing: 1. a unique name, 2. a Boolean value indicating whether it requires structure input or not, and 3. your already designed component. This has several optimization advantages as it registers your component function as a React or React Native element, allowing you to use hooks inside of it.
/* Wrapped in 'Component' functionconstGreeting = Component('Greeting',false,({name}={})=>{returnView()(Text()(`Hello ${name}!`));});constColorCard = Component('ColorCard',true,({color}={})=>structure=>{returnView({style:{backgroundColor:color,padding:10}})(structure);});
Let's see this concepts applied to our PasswordValidator component:
constPasswordValidator = Component('Password',false,({password,checks,config={}}={})=>{constchecksConfig = {minCharacters:{regex:/.{8,}/,text:'Be at least 8 characters'},numbers:{regex:/\d+/,text:'At least one number'},letters:{regex:/[a-z]/,text:'At least one letter'},capitalLetters:{regex:/[A-Z]/,text:'At least one capital letter'},symbols:{regex:/[!-/:-@[-`{-~]/,text:'At least one special character'},...config}password = password ?? '';constfailedChecks = checks.filter(check=> !password.match(checksConfig[check].regex));constisValid = failedChecks.length === 0;returnView({content:{direction:'column'}})([Text()('Requirements:'),failedChecks.map(check=>Text({list:'bullets'})(checksConfig[check].text))]);});
Attributes
When designing a component, the best practice is to explicitly name the properties that impact the output of your component, but at the same time, it is useful to have the flexibility to pass any number of standard attributes to your component. You can achieve this by providing the attributes as a rest parameter.
If you feed the attributes to your component’s structure, you can automatically inherit any number of properties, such as class, id, style, etc. You can see below how the PasswordValidator is updated to incorporate this:
constPasswordValidator = Component('Password',false,({password,checks,config={},...attributes}={})=>{constchecksConfig = {minCharacters:{regex:/.{8,}/,text:'Be at least 8 characters'},numbers:{regex:/\d+/,text:'At least one number'},letters:{regex:/[a-z]/,text:'At least one letter'},capitalLetters:{regex:/[A-Z]/,text:'At least one capital letter'},symbols:{regex:/[!-/:-@[-`{-~]/,text:'At least one special character'},...config}password = password ?? '';constfailedChecks = checks.filter(check=> !password.match(checksConfig[check].regex));constisValid = failedChecks.length === 0;returnView({content:{direction:'column'},...attributes})([Text()('Requirements:'),failedChecks.map(check=>Text({list:'bullets'})(checksConfig[check].text))]);});
Flavor
The best way to style a custom component is by leveraging the power of flavors. These allow you to create components that will inherit the look and feel of your app or any app they are placed in. Just provide a flavor property and read its variables to customize the style.
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
As you can see, now the component can be easily customized respecting the look and feel of the app. If you want, you can provide a default value to the flavor property, so that you don’t need to explicitly provide a flavor every time the component function is called. You can see how this is implemented in the PasswordValidator example:
constPasswordValidator = Component('Password',false,({password,checks,config={},flavor=readFlavor('default'),...attributes}={})=>{//By default, use the 'default' flavorconstchecksConfig = {minCharacters:{regex:/.{8,}/,text:'Be at least 8 characters'},numbers:{regex:/\d+/,text:'At least one number'},letters:{regex:/[a-z]/,text:'At least one letter'},capitalLetters:{regex:/[A-Z]/,text:'At least one capital letter'},symbols:{regex:/[!-/:-@[-`{-~]/,text:'At least one special character'},...config}password = password ?? '';constfailedChecks = checks.filter(check=> !password.match(checksConfig[check].regex));constisValid = failedChecks.length === 0;/* Create style using flavor */constflavorStyle = {backgroundColor:'white',borderRadius:flavor?.radius,borderColor:flavor?.borderColor,borderStyle:flavor?.borderStyle,borderWidth:flavor?.borderWidth,padding:15};/* Apply the flavor style to the view */returnView({content:{direction:'column'},style:flavorStyle,...attributes})([Text({style:{marginBottom:'5px'}})('Requirements:'),failedChecks.map(check=>Text({list:'bullets'})(checksConfig[check].text))]);});
Style
Even if you expose the customization of your component through flavors, you can have even more control by leveraging the style attribute. In the previous section, we have created styles based on the flavor properties. To enable further customization trough the style attribute, we need to merge both the flavor’s and the attribute’s styles. oneJS provides the mergeStyles function to this end; input the styles you want to merge in increasing priority and you will get the merged style as a result.
import{app,Component,readFlavor,mergeStyles}from'@onejs-dev/core';import{View,Text}from'@onejs-dev/components';constGreeting = Component('Greeting',false,({name,flavor,...attributes}={})=>{constviewStyle = {padding:10,borderRadius:flavor?.radius ?? 0,backgroundColor:flavor?.backgroundColor ?? 'transparent'};/* Styles are merged: attributes['style'] has priority over viewStyle */attributes['style'] = mergeStyles(viewStyle,attributes['style']);/* You can also explicitly describe the style priority as displayed below */consttextStyle = {color:attributes?.style?.color ?? flavor?.textColor ?? 'black'};returnView(attributes)(Text({style:textStyle})(`Hello ${name}!`));});constApp = ()=>View({content:{direction:'column'}})([Greeting({name:'oneJS',style:{backgroundColor:'pink'}}),Greeting({name:'oneJS',style:{color:'black'},flavor:readFlavor('light','primaryBackground')}),Greeting({name:'oneJS',flavor:readFlavor('light','rejectBackground')})]);app({component:App,theme:{preset:'oneJS'}});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
As you probably already expect, let's apply these learnings to the PasswordValidator component:
constPasswordValidator = Component('Password',false,({password,checks,config={},flavor=readFlavor('default'),...attributes}={})=>{constchecksConfig = {minCharacters:{regex:/.{8,}/,text:'Be at least 8 characters'},numbers:{regex:/\d+/,text:'At least one number'},letters:{regex:/[a-z]/,text:'At least one letter'},capitalLetters:{regex:/[A-Z]/,text:'At least one capital letter'},symbols:{regex:/[!-/:-@[-`{-~]/,text:'At least one special character'},...config}password = password ?? '';constfailedChecks = checks.filter(check=> !password.match(checksConfig[check].regex));constisValid = failedChecks.length === 0;constflavorStyle = {backgroundColor:'white',borderRadius:flavor?.radius,borderColor:flavor?.borderColor,borderStyle:flavor?.borderStyle,borderWidth:flavor?.borderWidth,padding:15};/* Merge the external style with the internal flavor style */attributes['style'] = mergeStyles(flavorStyle,attributes['style']);/* 'attributes' already contains the style attribute */returnView({content:{direction:'column'},...attributes})([Text({style:{marginBottom:'5px'}})('Requirements:'),failedChecks.map(check=>Text({list:'bullets'})(checksConfig[check].text))]);});
State
The final step to make a custom component interactive is giving it the ability to read and update the state of the app. As we have already established, a component should never directly update a state variable, instead, provide a property that takes a function. This function is then called by your component when a certain event occurs. For inputs, we name this function onChange as it is called when the value of the input changes. The user of this custom component can then decide if they want to provide a function to update the state or not.
See the example below where the onChange function in the custom Multiplier component is used to update the number state variable:
import{app,Component,read,update}from'@onejs-dev/core';import{View,Input,Text}from'@onejs-dev/components';conststate = {number:1};/* Multiplies the input number times the pressed button */constMultiplier = Component('Multiplier',false,({value,onChange}={})=>{constnumbers = [1,2,3,4,5,6,7,8,9];returnView()(numbers.map(number=>Input({type:'button',title:number,onClick:()=>{onChange(value * number)}})));});constApp = ()=>View({content:{direction:'column'}})([Text()('Result: ' + read('number')),Multiplier({value:read('number'),onChange:update('number')}),]);app({component:App,state:state,theme:{preset:'oneJS'}});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
With these notions we can finally complete the PasswordValidator component we have been using as an example:
import{app,read,update,Component,readFlavor,mergeStyles}from'@onejs-dev/core';import{View,Input,Text}from'@onejs-dev/components';/* State Configuration */conststate = {password:'',isValid:false};constPasswordValidator = Component('Password',false,({password,checks,config={},value,onChange=()=>{},flavor=readFlavor('default'),...attributes}={})=>{constchecksConfig = {minCharacters:{regex:/.{8,}/,text:'Be at least 8 characters'},numbers:{regex:/\d+/,text:'At least one number'},letters:{regex:/[a-z]/,text:'At least one letter'},capitalLetters:{regex:/[A-Z]/,text:'At least one capital letter'},symbols:{regex:/[!-/:-@[-`{-~]/,text:'At least one special character'},...config}password = password ?? '';constfailedChecks = checks.filter(check=> !password.match(checksConfig[check].regex));constisValid = failedChecks.length === 0;/* Trigger onChange function */if(value !== isValid)onChange(isValid);constflavorStyle = {backgroundColor:'white',borderRadius:flavor?.radius,borderColor:flavor?.borderColor,borderStyle:flavor?.borderStyle,borderWidth:flavor?.borderWidth,padding:15};attributes['style'] = mergeStyles(flavorStyle,attributes['style']);returnView({content:{direction:'column'},...attributes})([Text({style:{marginBottom:'5px'}})('Requirements:'),failedChecks.map(check=>Text({list:'bullets'})(checksConfig[check].text))]);});constApp = ()=>{returnView({content:{direction:'column',gap:10}})([Input({type:'password',value:read('password'),onChange:update('password'),placeholder:'Password'}),/* Bind to state variables */PasswordValidator({password:read('password'),checks:['minCharacters','numbers','letters'],value:read('isValid'),onChange:update('isValid')}),View({visible:read('isValid')})(Input({type:'button',title:'Strong password',flavor:readFlavor('accept')}))]);};/* App Function: Renders the App Component in the screen */app({component:App,state:state,theme:{preset:'oneJS'}});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
You are now prepared to create your own reusable components and start contributing to the oneJS community. We are excited to see what you are capable of creating.
You have now surpassed the equator of our units. In the next unit you will learn how to make your app navigable.
Unit 6: Routing
Routing is the process of selecting a path for data in a network. It enables the user to navigate the app and move between pages. The url (uniform resource locator) is the property that indicates what is the data to be fetched and displayed to the user; the active page. Through interactions such as clicking on links the user can change the active url to navigate to a different page.
Links
To enable routing, we need a way to change the current url. Well, in oneJS it is actually quite simple; just provide a value to the url property to any component and see how the url of the app changes when clicked.
import{app}from'@onejs-dev/core';import{View,Input}from'@onejs-dev/components';constApp = ()=>{returnView({content:{direction:'row',gap:10}})([Input({type:'button',title:'Home',url:'/'}),Input({type:'button',title:'Products',url:'/products'})]);};/* App Function: Renders the App Component in the screen */app({component:App,theme:{preset:'oneJS'}});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
Note: On the web, you can provide the url property to any component and click on it to change the url. On ios and android not all the components are clickable. If you are developing cross-platform, we recommend placing the url property exclusively on View or Input({type: 'button'}) components.
Naming Convention
How are url paths specified? In oneJS all urls are absolute; the full path to the page needs to be specified. The root url is /. All other urls also begin with / and are followed by the path to the target page. The segments of the route are also separated by /, e.g.: /path/to/page.
You may have heard about relative url trend, and it may sound attractive, but they need to know about the context they are defined in, breaking isolation. This is why we have not adopted them in oneJS, as it goes against the functional pattern.
View({url:'/products'})([/* DO NOT do this: Relative url */Input({type:'button, url: './product1'}),/* DO this: Absolute url */Input({type:'button, url: '/products/product1'})]);
Show and Hide
You now know how to change the current url but routing also involves displaying and hiding information on the screen based on the active url. You can modify the visibility of a View, just by feeding the visible property. Remember the Visibility section from Unit 4.
The only thing left to connect the pieces, is how to use the current url to change the visibility of View components. Fortunately, oneJS provides a simple way of doing so. Since the url is a property that changes and affects the behavior of your app, it needs to be part of the state. Just specify the url to be matched as the source for your state variable; when it matches it will automatically become true and when it does not it will be false. You can use asterisks (*) in your path to indicate that any value is acceptable for a certain segment. It is way easier to see in action in the example below:
import{app,read}from'@onejs-dev/core';import{View,Input,Text}from'@onejs-dev/components';conststate = {homePage:{source:{url:'/'}},productsPage:{source:{url:'/products'}}}constApp = ()=>{returnView({content:{direction:'column',gap:10}})([View()([Input({type:'button',title:'Home',url:'/'}),Input({type:'button',title:'Products',url:'/products'})]),View({visible:read('homePage')})(Text()('Home Page')),View({visible:read('productsPage')})(Text()('Products Page')),]);};/* App Function: Renders the App Component in the screen */app({component:App,state:state,theme:{preset:'oneJS'}});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
Behind the scenes oneJS is using the history API on the web, so you can seamlessly go back and forward in your url history.
Advanced Routing
There are complex scenarios where you want to render a list of items and dynamically generate the url on click. In these cases, you have no way of knowing in advance the full url, as it usually depends on the database id of the selected element. To address this, you can provide to the state variable source, a url containing a : value for the segment you want to retrieve data from. For instance, if the url source is products/: and the actual url is products/product1, the state variable will be equal to product1. You can test this in the example below:
import{app,read,readFlavor}from'@onejs-dev/core';import{View,Input,Text}from'@onejs-dev/components';/* Variable in path to fetch the product id */conststate = {productId:{source:{url:'/:'}}};/* Emulates a 'products' database response */constproducts = {product1:{name:'pizza',price:'$10'},product2:{name:'risotto',price:'$15'},product3:{name:'tiramisu',price:'$5'}};constApp = ()=>{return[Object.keys(products).map(product=>Input({type:'button',title:product,url:'/' + product})),read('productId') && View({content:{direction:'column',gap:10},flavor:readFlavor('shadow')})([Text()('Id: ' + read('productId')),Text()('Name: ' + products[read('productId')].name),Text()('Price: ' + products[read('productId')].price),]),];};/* App Function: Renders the App Component in the screen */app({component:App,state:state,theme:{preset:'oneJS'}});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
If you have followed this far, you are now able to create an interactive page enabling the user to serve different content based on their actions. Head to the following unit and learn how to add fantastic animations!
Unit 7: Animations
So far, your app is stylish, navigable, and well structured. You could leave it here, but animations often mark the difference between beginners and pros. In this unit you will learn how to use out of the box animations with View components.
Property Id
An animation is a change in visual properties that happens over a certain period of time. It must be triggered by a change in your components’ properties, and therefore, by a change in the state. These are the most common properties that are used to trigger animations:
visible: Use this property to animate a component that becomes visible or invisible.
intersect: Use this property to animate an element as it enters (intersects) the screen. It is mainly used with scrolling views where an element becomes visible when the user reaches its position. It is a web-only property.
The way to specify animations in your View components is by providing a configuration object to the animation property. This object specifies which property triggers the animation and which animation will be performed for each value. In the example below, the View will be animated when the property visible changes; if true it will fade-in, and if false it will fade-out:
For Boolean variables you can use an array with two positions, as shown in the previous example. The first position, represents the animation to be displayed when true, and the second, when false. The example below builds upon the one from Unit 4, Visibility section, and adds an animation to the visibility property:
import{app,read,update,readFlavor}from'@onejs-dev/core';import{View,Input,Text}from'@onejs-dev/components';conststate = {isVisible:true};constApp = ()=>{returnView({content:{gap:10}})([Input({type:'switch',value:read('isVisible'),onChange:update('isVisible')}),View({visible:read('isVisible'),flavor:readFlavor('primaryGradient'),style:{padding:40},/* Defining the animation */animation:{visible:['fade-in','fade-out']}})(Text({flavor:readFlavor('light')})('Toggle visibility'))]);};app({component:App,state:state,theme:{preset:'oneJS'}});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
Other Types of Properties
If you want to animate a property that takes non-Boolean values, create a JavaScript object and provide an animation for each value:
animation:{customProperty:{
a:'fade-in',//When 'customProperty' is equal to 'a' the animation 'fade-in' is triggeredb:'fade-out',//When 'customProperty' is equal to 'b' the animation 'fade-out' is triggeredc:'expand'//When 'customProperty' is equal to 'c' the animation 'expand' is triggered}}
You can see this in action in the example below:
import{app,read,update,readFlavor}from'@onejs-dev/core';import{View,Input,Text}from'@onejs-dev/components';conststate = {choice:''};constoptions = ['a','b','c'];constApp = ()=>{returnView({content:{gap:10}})([Input({type:'list',options:options,value:read('choice'),onChange:update('choice')}),View({/* Property triggering the animations */customProperty:read('choice'),flavor:readFlavor('primaryGradient'),style:{padding:40},/* Defining the animation */animation:{customProperty:{a:'fade-in',b:'fade-out',c:'expand'}}})(Text({flavor:readFlavor('light')})('Animated Block'))]);};app({component:App,state:state,theme:{preset:'oneJS'}});
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
Preset Animations
To make your life a bit easier, oneJS provides several standard animations. In the previous examples, we have already displayed some of them, such as fade-in, fade-out and expand. You can use the preset animations just by indicating their name as a string in the animation property. The example below showcases the available presets at the moment. We are creating more for future releases. We would love to hear your opinion!
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
Custom Animations
Preset animations are great, but you can also choose to create your own. Under the hood, oneJS is using the standard Web Animations API for the web and React Native’s Animated API for iOS and Android. This means that if you are an advanced user, you can create your own animations using the following parameters:
keyframes: They each of the states on a timeline which mark the beginning or end of a transition. Keyframes define the properties to be interpolated in the animation and their values for each of the steps. For example, in a fade-in animation, the opacity property will be interpolated from 0 (transparent) to 1 (opaque).
options: They are the object describing how the animation will be executed. You can specify parameters like the duration of the animation in milliseconds, or the easing; the process of making an animation not so severe or pronounced by defining the curve to interpolate the animation. You can also provide a style object indicating the style to be applied after the animation has ended. There are many more options besides these; make sure to refer to the Web Animations API and React Native’s Animated API for more information.
The example below showcases how a fade-in-left and shrink animation definition would look like in the web. Please, note that the way transform keyframes and easing are specified in React Native differ from web.
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
That’s all there is to it. Use the animation property in your View components to breathe new life into your app.
Unit 8: Storage
In this unit you will complete your knowledge to begin your journey as a oneJS developer. Throughout this unit we will discuss the different types of storage. Storage is in fact what it seems, the process of storing and persisting data even if the app is closed. Depending on the topology (the location of the data) we can divide storage in local, when data is stored in the user’s device, or online, when data is stored in an external database.
Local Storage
Let’s begin by exploring localstorage. The typical use-case for this type of storage is possibly saving user specific configuration. Suppose our app lets the user choose the default language. You can leverage this type of storage to retrieve this value every time the user launches the app.
To persist data, first choose the variable you would like to persist and make it part of the state, if it is not already. Then, specify the local type of storage and give it an id. This id will be used to store the variable. After this step, your variable will be saved every time its value is modified. Now, if you want to retrieve its value when the app is loaded, you need to define the source. Just define local as the source providing the same id as the one used in the storage.
Try to give a unique name to your ids, since local storage is shared by other apps running in your device. See the example below putting this in practice:
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
You can refresh the page and see how the selection in the example above persists. Internally, oneJS leverages the Storage API for the web and Async Storage package for iOS and Android. Using this type of storage you can save simple key-value pairs, but it is not adequate for complex data models. Keep reading to learn how to manage complex scenarios.
Online Storage
For some apps local storage may be enough, but as your app grows in complexity you may find it necessary to use an external database to store your data. oneJS support Firebase's Cloud Firestore database out of the box. It follows a document-oriented model as described on their website:
Data is stored in documents, which are organized into collections. All documents must be stored in collections.
Each document contains a set of key-value pairs we refer to as data. Documents can contain subcollections and nested objects, both of which can include primitive fields like strings or complex objects like lists.
Cloud Firestore is optimized for storing large collections of small documents.
To use Cloud Firestore in your app, you first need to initialize your database. First, import Cloud Firestore in your project and call the initializeApp function with the configuration object corresponding to your project. Then, call the getFirestore function using the output of the previous function. This function returns the configured Firestore database object. It should look something like the example below:
import{initializeApp}from'firebase/app';import{getFirestore}from'firebase/firestore';// TODO: Replace the following with your app's Firebase project configuration// See: https://firebase.google.com/docs/web/learn-more#config-objectconstfirebaseConfig = {// ...};// Initialize Firebaseconstapp = initializeApp(firebaseConfig);// Initialize Cloud Firestore and get a reference to the serviceconstdb = getFirestore(app);
Once you have the Cloud Firestore database object (db in the example), feed it to the app function’s firestore property. This enables oneJS to perform read and write operations.
Finally, create a state variable that uses firestore as its source, storage or both. Indicate the path to the document or collection that you want to retrieve, create or modify. For documents, you can include state variables in your path by wrapping their name in < >. See the example below:
oneJS syncs the state with the database data automatically. This way, you can focus on modifying the state of your app, and oneJS will perform the dirty work for you.
We are working to support more popular serverless databases. We would love to have feedback from you!
IndexedDB (web-only)
IndexedDB is an in-browser database that allows you to store significant amounts of structured data, including files/blobs. It is supported by most modern browsers and uses indexes to enable high-performance searches. While the Local Storage discussed in the previous section is useful for storing smaller amounts of data, it is less adequate for storing larger amounts of structured data.
IndexedDB uses a data model based on object stores and transactions. You can read all about it in the MDM Web Docs. For simplicity, you can think of it as Cloud Firestore's document-oriented model; the root level is a collection that can contain any number of documents. Documents are JavaScript objects that store key-value pairs. Unlike Cloud Firestore, IndexedDB does not allow collection nesting or subcollections.
Using IndexedDB in oneJS requires no set up, just create a state variable using IndexedDB as your source and storage indicating the path to the data. You can then modify the state variable and oneJS will sync updates to the IndexedDB database for you.
Working with more complex data also requires additional tools to update your data. In the previous units, we have displayed how to use the update function; it takes the state variable's id and replaces its value when an event is triggered:
When working with databases, you may just want to update a certain document within a collection and not the entire collection. You can achieve this with the update function, just provide a second argument, in addition to the state id, which represents the document id to be updated:
You may also want to add a new document into a collection without needing to overwrite the entire collection. In this case, you can use the add function. Just provide the state id of the collection and a new document will be created when the add function is triggered:
Finally, to complete the CRUD operations, oneJS gives you the power to remove data from your database. Beware that with great power comes great responsibility 🕷. To delete an item from a collection, call the remove function with the desired document id:
Let's see all this concepts in a real example; a TODO app. Do you know any other frameworks where you can write this in less than 30 lines of code?. Please let us know!
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
Well done! You have completed all the learning units, use this knowledge to build something amazing. You can head to the Playground section to test your ideas or check out the Docs for more in-depth content.
To be continued… More units with more incredible features are coming.