Catalyst Developer
Manual Setup
- Extract or git checkout Catalyst somewhere outside your web root
- Inside your web root do the following:
- Create a directory called data [owner: web user, perms 0770]
- Create a directory called themes [owner: web user, perms 0770]
- Create a symlink called catalyst that points to the “public” directory inside Catalyst
- Copy all files from the Catalyst install/public directory
NOTE the .htaccess file which may be hidden[owner: web user, perms 0640]
- One level above your web root create a directory called catalyst [owner: web user, perms 0770] and inside it:
- Create a directory called data [owner: web user, perms 0770]
- Create a directory called log [owner: web user, perms 0770]
- Create a directory called conf [owner: web user, perms 0770] and inside it:
- Copy ccms-config.php from the Catalyst install/private directory and modify it accordingly [owner: web user, perms 0660]
Routes
Read up on BulletPHP.
Default routes are set in Catalyst. Such as /ccms, /rpc.
Front end routes can be overriden from apps and the htdocs/catalyst site specific directory
- /htdocs/catalyst/routes.php // Custom routes for the client’s website
- /app/myapp/frontend-routes.php // Can override all front end routes and custom routes
Front end routes can clash. Be careful building an app that can potientially break another app’s routes. This can result in the user becoming frustrated and uninstalling your app.
Custom front end routes set in the site directory (/htdocs/catalyst) can be used for overriding the default home page message set by catalyst or, if you’re a coder, can be used for better customization of your front end in use with custom code. This would normally be code you quickly want to add for a specific website, where building a new app doesn’t make sense or is overkill.
Setup
- Extract or git checkout Catalyst somewhere outside your web root
- Inside your web root you need 3 files: – A symlink called catalyst thawt points to the “public” dir inside Catalyst – Copy both index.php and .htaccess from the ctalyst install/files directory
How to CCMS3 Client Side to Server Side
- conf.js: Nav and conf stuff in conf.js
- routes.js: Page routes are conf’d in routes.js
- pages.js: Page callbacks in pages.js (And can be split up into other files if they get too big)
- shared.js: Re-useables (like login() which can be used if multiple areas) in shared.js
- app.js: App setup in app.js
Libs that are good to know
- Ractive.js - http://docs.ractivejs.org/latest/get-started
- Page.js - https://visionmedia.github.io/page.js/
Useful PHP constants
In most cases Catalyst can figure out it’s own environment. If it is wrong, you can add define() calls to index.php that will override the defaults.
Important to notice if the constant ends with _DIR it refers to a filesystem path. _URL is pretty self-explanatory
- BASE_URL is the top level of where Catalyst is accessible from the web. – http://www.example.org [Catalyst is installed in the web root of www.example.org] – https://subd.example.org/mysite [Catalyst is installed in a subdomain, and inside a directory] -WWW_DIR is where web-accessible files reside on the filesystem. -PRIVATE_DIR is where non web-accessible user data is kept -CATALYST_DIR is where the non-web-accessible files for Catalyst exist.
api() function
api(full route string, javascript object params).then(success function, error function);
sends the following:
- params is converted to JSON and sent via post as data={ … }, so in your PHP script you’d reference data like $data = json_decode($_POST[“data”], true);
- On return it’s important you use the following format:
{
"results": any variable type,
"error": null/false or non-falsy value telling about the error
}
Must be utf-8 application/json content type and charset.
Conf
Found in conf.js
nav: {
items: [
{
title: "Pages",
path: "/pages",
icon: "pages"
},
{
title: "Editor",
path: "/editor",
icon: "editor"
},
{
title: "Apps",
path: "/apps",
icon: "apps"
},
{
title: "Settings",
path: "/settings",
icon: "settings"
},
{
title: "My Account",
path: "/myAccount",
icon: "myAccount"
}
]
}
- Title is anchor test
- path is appended to root e.g. dogsandcats.com/ccms/my-path-here
- icon references one of the image icons in the img white icons folder
// e.g. dogsandcats.com/ccms3
conf.basePath = "/ccms3";
Routes
Client side routes are set in routes.js
var routes = [
{ path: "/", page: "home" },
{ path: "/pages", page: "pages" },
{ path: "/pages/edit/:id?", page: "editPage" },
{ path: "/app/:id", page: "app" },
{ path: "/app/:id/item/:id", page: "appForm" }
];
/ is root/home of your conf.basePath.
path: is a matched string. page: is the pages method you’re referencing which is called by openPage() in app.js. Your pages are found in pages.js
Pages
Each page method is used as a callback for a route. Each of these methods must return a promise.
var initPages = function initPages(view) {
return {
home: function home() {
return new Promise(function(resolve, reject) {
resolve("burp");
});
},
myCustomPage: function myCustomPage() {
return view.set("someStuff", "x"); // Set returns promise, so is cool
}
...
You mainly would use these page functions for setting data that has to display on the page.
openPage calls these functions, first setting the loading variable and then wating for the promise to resolve, where it then sets loading to false and shows the content.
So inside the page function you can call async tasks as long as they resolve when done.
Why does it need to return a promise? Because it makes sense to allow the page to load the data is needs while the loader is showing. Async functions like api() can take any random amount of time to resolve based on server load and other factors. This allows everything to work is proper sequence between page switches.
Events
Your events.js file has all of the Ractive.js events. This function is called in the Ractive or view context so you can use this or view when referencing the Ractive instance.
In the HTML, you’re event works like this on-something=“myEvent:‘pow’”
In your events.js object, add the function like so:
myEvent: function(event, param) {
alert("myEvent passed " + param + " at " + event.keypath);
},
...
Forms
I’ve set up the forms in the markup so it can easily copied. The concept is pretty straight forward.
In the Javascript, you need to set some variables to make it work. So when you’re page loads you need to set a variable that reads the form data in the template:
view.set("form", {
tabs: [
{
title: "Basic"
},
{
title: "Permissions"
},
{
title: "Custom Fields"
}
],
fieldset: 1
});
tabsare used to switch between fieldsets. feildset can be used to set the default tab you want to view when the form loads. Setting form.feildset sets which tab in tabs by referencing the array index + 1. So in this example, tab title “Permissions” can be set by view.set(“form.fieldset”, 2);
The form data can be set to any variable. You’ll want to use two-way binding to set values in an object that you can later use to update the server with. For example, <input value="{{currentPage.title}}"> will automatically create/update that key path. currentPage or parts of it, can later be sent to the server, a in-browser DB, cache using whichever method allows to store key/value pairs.
App: Grid views
Here is a snippet of an app function:
return view.set("app", {
title: "GoCart",
nav: { // Title: dataview
"Products": "products",
"Tax Codes": "tax_codes",
"Comments": "comments"
},
collection: {
name: "products",
title: "Products",
fields: {
"photo": {
out: function(val) {
return "<img src='" + val + "'>";
},
title: "Photo"
},
"name": { title: "Product" },
"price": { title: "Price(CAD)" },
"summary": {
out: function(val) {
return (val || "").substr(0, 30) + "...";
},
title: "Summary"
}
},
items: [
{
photo: "http://lorempixel.com/100/100/",
name: "Beach Ball",
price: 9.99,
summary: "Donec consectetur facilisis ex ut porttitor. Mauris sed mollis nibh. Ut interdum congue vehicula. Praesent varius id risus ac ultrices. Proin nec metus facilisis nisi semper egestas quis quis eros. Nunc id magna massa. Sed eget egestas nunc. Cras a felis sed ex viverra elementum dignissim at urna."
},
{
photo: "http://lorempixel.com/100/100/",
name: "Beach Towel",
price: 19.99,
summary: "Donec consectetur facilisis ex ut porttitor. Mauris sed mollis nibh. Ut interdum congue vehicula. Praesent varius id risus ac ultrices. Proin nec metus facilisis nisi semper egestas quis quis eros. Nunc id magna massa. Sed eget egestas nunc. Cras a felis sed ex viverra elementum dignissim at urna."
},
{
photo: "http://lorempixel.com/100/100/",
name: "Plastic Nom",
price: 99.99,
summary: "Donec consectetur facilisis ex ut porttitor. Mauris sed mollis nibh. Ut interdum congue vehicula. Praesent varius id risus ac ultrices. Proin nec metus facilisis nisi semper egestas quis quis eros. Nunc id magna massa. Sed eget egestas nunc. Cras a felis sed ex viverra elementum dignissim at urna."
},
{
photo: "http://lorempixel.com/100/100/",
name: "Nexus Plus",
price: 789.99,
summary: "Donec consectetur facilisis ex ut porttitor. Mauris sed mollis nibh. Ut interdum congue vehicula. Praesent varius id risus ac ultrices. Proin nec metus facilisis nisi semper egestas quis quis eros. Nunc id magna massa. Sed eget egestas nunc. Cras a felis sed ex viverra elementum dignissim at urna."
}
]
}
});
The nav is simply the app nav. The collection or “current collection” is simply placed there based on the data loaded. We can overwrite this each time switch between app sections/pages.
Our collection is the part config, part raw data. The config part can be loaded into the browser before hand. The data will have to come from a data source.
name is for reference sake when communicating back to the server…and title of collection is what we show the user.
fields are the configuration of the column titles in the grid. Each field conf is an object with the following properties:
title: "What you want the user to see in the grid column title"
out: a function that allows you to format the output. This does nothing to the original data. Exluding this function will output the raw value.
The items array is the data you’ve grabbed from your data source, most likely your api/server. Keys are matched to the field conf key names in the order they’re looped out from the fields object. The displayed keys of the items objects will only show keys that match the fields keys, otherwise they’ll be exlcuded from the grid display.
App: Forms
return view.set("app.appForm", {
title: "Product",
collection: "products",
fields: [
{
label: {
title: "Title"
},
element: {
tag: "input",
type: "text",
placeholder: "Title"
},
name: "title", // name of variable collection item
val: "123"
},
{
label: {
title: "In Stock"
},
element: {
tag: "input",
type: "number",
placeholder: "In Stock"
},
name: "in_stock", // name of variable collection item
val: "2"
},
{
label: {
title: "Hidden"
},
element: {
tag: "select",
placeholder: "Choose an option",
options: {
"1": "Yes",
"0": "No"
}
},
name: "hidden", // name of variable collection item
val: "0"
},
{
label: {
title: "Text Size"
},
element: {
tag: "ratio",
options: {
"12px": "Small",
"20px": "Big"
}
},
name: "text_size", // name of variable collection item
val: "0"
},
{
label: {
title: "Market"
},
element: {
tag: "checkbox",
options: {
"kids": "Kids",
"women": "Women",
"men": "Men",
"teens": "Teens"
}
},
name: "market", // name of variable collection item
val: "0"
}
]
});
title is the h1 tag title shown above the form.
collection can be used to identify the which table/collection the data belongs to which can later be used in the api.
fields is an array of field objects that contain config and data from the two-way binding. These will loop out to draw your form.
feilds[].label.title is the title of the <label> text above the feild.
feilds[].element configs the element like so:
{
tag: "input|select|textarea",
type: "any valid html5 type(only works with inputs)",
placeholder: "Will be used as the default select option and the placeholder tag in an input"
}
feilds[].name is the name of the key/field in the collection.
feilds[].val is the value of the field in the collection. This is automatically updated via two-way binding in the form. You can also set default inputs by setting it manually.
feilds[].options is for selects, checkboxes and radios. They are a key/val pair and work like so:
{
"m": "Man",
"f": "Woman"
}
Events and Event APIs
Events can be used around Catalyst to allow other modules to integrate or be notified. Each event can have its own API, or in otherwords, the type of data it sends and if that data can be manipulated or not.
The “API” of an event can be data, accessable objects or special functions exposed by the event to extend functionality to the integrating app.
For example the Form app can fire an event called “form/submitFormPreValidate” to signal any apps listening for that event. The app listening for that event can then access the event API data provided by the form app to make changes or make a last minute check before it continues.
Each event can provide the listener with a different kind of data to the next. Not every event sends the same type of data or allows all of it to be modified.
Each event should be documented so that there is no confusion in what each event includes in its API.
Naming convention
Each event must be named in the following fashion:
appName\nameOfEvent
If the event is not being sent by an app, it would start with a backslash. Indicating that it is a core event.
\nameOfEvent
Documenting Events
Each app must include a section in the README.md file listing the events sent by that app and documenting their APIs.
Listening for Events
Each app that listens for events must have an listeners.php in their root directory.
The contents of listeners.php may go as follows:
<?php
$myEventHandler = function($api) {
};
// We can use this for integration setup
\Atom\Events::On("website\listIntegrations", function($api) {
$api->add([
"title": "Fancy SEO Pack",
"description": "...",
"appName": "fancySEO",
"events": [
"website\drawHeader" => "myEventHandler" // if installed, we'll insert this to the db, and reference the function back to this listeners.php file by checking appName
]
]);
});
// These handlers can be used later, mapped to the db when someone inserts an integration
return [
"fancySEO\myEventHandler" => $myEventHandler
];
?>
Firing Events
Events can be fired using the \Atom\Events::fire(string, any)
Example:
$data["codeBlocks"] = ...;
$addCodeBlock = function($string) use(&$data) {
$data["codeBlocks"][] = $string;
};
\Atom\Events::Fire("website\parsingCodeBlocks", [ "data" => &$data, "addCodeBlock" => $addCodeBlock ]);
This can later be used by an integration to add code blocks before the output is drawen.
Installing events
By default events can be listened for by adding \Atom\Events::On listeners to your listeners.php. However, this doesn’t work if we don’t want to enable an integration or want to have it disabled by default.
What needs to happen for integrations that need installation is to save the events in the db. Once we no longer need the event, we simply delete it.
Saved event listeners will be listened for while Catalyst finds all of the listeners.php files in each app and can access the returned array of functions.
Create an event:
$event = new \Catalyst\Event($db);
$event->setName("website\parsingHeader");
$event->setHandler("fancySEO\myEventHandler");
$event->setGroup("fancySEOWebsite");
$event->save();
Later we can disable the integration by deleting event with group of “fancySEOWebsite”.