Monday, September 9, 2019

App, main and other controller accessors

When I've started flow, one of the features I wanted for it was scaffolding from the front-end and be able to do things like:
Flow scaffold model: #User
or:
Flow scaffold crudFor: #Task
and have created the Model and Controller classes and accessors in environment so you can continue developing the app pulling things from there.
There are several reasons to have scaffolding features, the utilitarian and most pragmatic reason is that it helps you to show something useful fast. A cheap positive feedback loop that allows you to scaffold the next basic feature of your app. That's something really aligned with flow's mission regarding to productivity.
A secondary good reason is that scaffolding makes it easier to spread basic good practices. It shows by example to new people how to get new models and controllers in an application that can scale in complexity with elegance and high maintainability.
But I'm not implementing scaffolding just yet.
The reason why is not being done yet is because we are still in discovery mode of what are those practices. The idea is to really confirm if they are common, general and frequent enough.
In this article, I'm going to share the current best candidates for those good practices. If I'd have to implement the scaffolding system today, I'd make it build the App and its main controller, the main controller accessors, models and router and here I'd show you how. Also we'll cover here what's the suggested convention to name things and why is convenient to do it that way.
App and main Since version 0.2.7, a flow-based application has the idea of App and main. A default clone of the flow repo will use the App class and MainController class for a sample tutorial-like application.
The App class is a convenient starting point to access and "talk with your app" whatever app it is. In flow we asume you'll have to investigate things on your app and its controllers so we are trying to make that easy for you. One way is providing easy navigation. Here is how you access the main controller of your application which is the root of the controller's graph. "Used by index.html to start your app."
App start.
"Deals with routes definition and is part of the starting process of your frontend." App setupRouter.
"Accessor to the instance of MainController of your app." App main.
Take the last one for example, it's 2 words, one of 3 characters the other has 4. Really friendly to type. Amber doesn't have autocomplete so by choosing two short words, we are lowering the friction to access the root controller of your application so you can easily inspect, send commands and see how it reacts. Is safe to say it will be used a lot so you'll profit a lot. For example, an inspect on that code allows you to navigate to any controller that got currently instantiated. That makes easy diagnose possible issues and prototype live-coding the next feature.
The MainController should have as minimum two methods: #home and #reset. You should be able to do:
"Get to the default state on the main controller." App main reset.
"Answers the 'home' controller of the app." App main home.
Here is code that implements the #reset method taken from a real application: MainController>>reset
self navbar deactivateAll.
self hideAllBut: #navbar.
self home show
That reset method makes the app to instantly show the home without any blinks on the screen.
Accessors Now take a look at the #home accessor
MainController>>home "Answers the home controller of the app. Creates it lazily if needed." ^ self ifAbsentAt: #home put: [ HomeController for: model on: self appendingTo: ‘#home-wrapper’ asJQuery ]
So by just naming it with App home, you get it created. Once created, you get that instance as answer every time. That's as close as you can be to zero friction between developer's wish and reality (machine's answer). Just the way things should be. Note we are using here the class method to instantiate controllers: #for:on:appendingTo:
Controller class>>for: aModel on: aParentControllerOrNil appendingTo: aHtmlElement “Answers a new instance of this controller dedicated to aModel, child of aParentControllerOrNil and meant to be appended to aHtmlElement.” ^ self new model: aModel; parent: aParentControllerOrNil; parentElement: aHtmlElement; yourself
Lets take a look at what it expects:
  • In the for: part it expects the model for the new controller
  • In the on: part it expects the parent controller, a MainController in this case.
Note that the parent controller has sense to be nil only when you instantiate the MainController which is a controller that is the root of the whole graph of controllers.
In the appendingTo: part it expects a DOM element to be the parent element of whatever that new controller needs to render. Typically that would be a
with an id named consistently with the controller class name plus the 'wrapper' suffix as you see in that snippet. The tutorial sample application of a default flow clone has 5 subcontrollers covering from the most basic template based controllers to two-way data binding, composition and client-server RESTful interactions. On its MainController you find 5 accessors: example1, example2, etc. up to example5. They all use the same idea of this #home accessor.
Models
Models in flow are really friendly. Creating the class is enough to start using them. They are Mapless objects which means you don't really need to declare in advance what instVars and accessors they need to have in order to be usable by your application. This makes really easy to work in a prototypical way encouraging discovery and experimentation, two really strategic features for innovation projects and startups. Any subclass of Model you add is going to be a Mapless. So basically you only care about the model name and add all your model classes. Later you add the methods that makes them to have special behaviour. As general rule, Mapless don't need setters and getters, they use DNU to get and set automatically whatever you tell them to. They will answer nil when you ask for something they don't have. In the following snippet we create an instance of the Visitor class, we set the language according to the browser and we save it in the backend:
"Create a model of the visitor, set its current language and saves it in the backend." | visitor | visitor := Visitor new. visitor language: window navigator language. visitor save. This feature of DNU and returning nil makes them a bit different if you want to have lazy initializations on some of their properties. How does it look a lazy initialization in a mapless? Here is another example taken from real code: Order>>products "Answers the products in this order. Creates the empty collection if needed." super products ifNil: [ self products: OrderedCollection new ].
^ super products
Routes
Lets dig now on the routing system used in flow applications. It all begins at load time. When the frontend is being loaded by your browser using RequireJS, it comes to a moment where it installs the 'App' package containing the App class and all the other classes and methods of your application.
When Amber installs classes on the package loading process, the classes will receive the initialize message on its class side. If there is none, nothing special will happen but if your class implements it, it will perform those custom actions.
In flow, we use this:
App class>>initialize self setupRouter
So even before he application starts, it already will have the router programmed to react correctly to whatever your app should do with the given initial and following URIs.
Lets see the setupRouter now:
App class>>setupRouter "Program the application reactions for different URI routes." Router rlite "Normalization of the home route." add: ‘’ do: [ :r | Router set: ‘#/home’ ]; add: ‘#’ do: [ :r | Router set: ‘#/home’ ]; add: ‘#/’ do: [ :r | Router set: ‘#/home’ ]; add: ‘/’ do: [ :r | Router set: ‘#/home’ ]; add: ‘home’ do: [ :r | App main reset ]; "Reactions related to Contact models" add: ‘contacts’ do: [ :r | App main showContacts ]; add: ‘contacts/:id’ do: [ :r | Router set: ‘#/contacts/’,r params id,’/view’ ]; add: ‘contacts/new’ do: [ :r | App main showNewContact ]; add: ‘contacts/:id/view’ do: [ :r | App main showContactId: r params id ]; add: ‘contacts/:id/edit’ do: [ :r | App main editContactId: r params id ]; add: ‘products’ do: [ :r | App main showProducts ]; add: ‘products/:id’ do: [ :r | Router set: ‘#/products/’,r params id,’/view’ ]; add: ‘products/:id/view’ do: [ :r | App main showProductId: r params id ]; add: ‘search/:target’ do: [ :r | App main showSearchResultsFor: r params target ]; yourself
At this point you'll see that the code is quite self explanatory which is one of the guiding principles behind flow. In the same way this example uses #showNewContact, #showContactId: and editContactId: you would do different routes for different models on your own application. Bonus As a bonus, here is how #editContactId: looks like:
MainController>>editContactId: anId self hideAllBut: #navbar. self contactEditor editId: anId
The editor accessor that you already can infer:
MainController>>contactEditor
^ self ifAbsentAt: #contactEditor put: [ ContactEditorController on: self appendingTo: ‘#contact-editor-wrapper’ asJQuery ]
The #editId: on the editor controller taking the model from the backend:
ContactEditorController>>editId: anId
self showPreloader. Person findOne: #{ ‘_id’ -> anId } then: [ :person | self editContact: person. self hidePreloader ] ifNone: [ ] onError: [ :res | self showError ]
And finally:
ContactEditorController>>editContact: aPerson
self model: aPerson. self show done: [ "specific stuff" ]
The #show method in Controller always return a promise so you can do your own custom stuff when the view gets created asynchronously without complicating your code. So we don't have yet scaffolding today (January 2015) but I hope this article encourages you to write highly readable code on flow.
Happy coding!

No comments:

Post a Comment