Skip to content
This repository was archived by the owner on Apr 20, 2024. It is now read-only.
This repository was archived by the owner on Apr 20, 2024. It is now read-only.

Submissions improvements and documentation #48

@Magiguigui

Description

@Magiguigui

Hi @Siemen and everyone,

Am currently working with Vapor and found the few nodes-vapor's
framework indispensable !

This is more a general comment than a proper issue.
But I find very difficult to find examples/docs or proper getting started
about the frameworks, that's kind of a shame when you see the power
of all that tools...

Let me explain everything :

For each form : you can represent it in swift code by two functions of a controller :

func renderView(_ request: Request) throws -> Future<View>
func handleForm(_ request: Request) throws -> Future<Response>

The renderView method, in a Submissions context, provide the fields to the request cache.
The handleForm method is charged to validate the form and redirect depending of results...
I've never found it explained somewhere that you had to also render the view and encode
it to a response in the handler function cause when you code with vapor you take the habit to
redirect in form handlers and render in renderer views.

...
.catchFlatMap { error in
	guard error is SubmissionValidationError else {
		throw error
	}

	return try request
		.view()
		.render(leafPath, on: request)
		.encode(for: request)
}

The same kind of misunderstanding happend with the SubmissionsMiddleware for example :

middlewares.use(SubmissionsMiddleware())

Why it is the only middleware that need to be instantiate ? Every other middleware are registered through class type.

Same thing with the model's Submitable, Creatable, Updatable, ... protocols that really make things easier:
but you have to find their behaviors by yourself because it's never explained how to use it.

So after hours of retro-engineering I think that all nodes-vapor's framework should improve their documentation.

Now I would like to propose some enhancement for Submissions.

The automatic Form

Idea: Be able, through leaf tags, to generate an entire form as you can generate a form-input.

I did something like that through a new Tag #form().

Problems: For the moment the Tag look like #form([String]) because you can't access request.fieldCache.fields (internal).

Workaround: Pass the fields keys through context and retrieve it in TagRenderer to have a complete list of Field.

To make your own TagRenderer you have to access SubmissionsData and InputData initializers, but their are internals so you have to extend them too.

public struct Context: Codable {

    // MARK: - Properties
    public var form: [String]


    // MARK: - Initialization
    public init(_ model: Model) throws {
        self.form = try model.makeFields().map { return $0.key }
    }

}

...

let context = try Context(User.self)
try request.addFields(forType: User.self)

return try request
    .view()
    .render(leafPath, context, on: request)

...

extension TagContext {

	func submissionsDatas() throws -> [SubmissionsData] {
		let fieldCache = try requireRequest().fieldCache()

		guard let form = self.parameters[safe: 0]?.array else {
			throw self.error(reason: "Unsupported key type")
		}

		var data: [SubmissionsData] = []

		for formField in form {
			let fieldName = formField.string ?? ""

			let keyPath = fieldName.components(separatedBy: "[]")[0]
			var field = fieldCache[valueFor: keyPath]
			let errors = fieldCache[errorsFor: keyPath]

			data.append(SubmissionsData(fieldName, field: &field, errors: errors))
		}

		return data
    }


}

extension TagContext.SubmissionsData {

	// Mark: - Context
    struct InputData: Encodable {
        let key: String
        let value: String?
        let label: String?
        let isRequired: Bool
        let errors: [String]
        let hasErrors: Bool
        let placeholder: String?
        let helpText: String?

		// MARK: - Initialization
		init(_ submissionsData: TagContext.SubmissionsData, errors: [String]) {
			self.key = submissionsData.key
			self.value = submissionsData.value
			self.label = submissionsData.label
			self.isRequired = submissionsData.isRequired
			self.errors = errors
			self.hasErrors = !errors.isEmpty
			self.placeholder = submissionsData.label ?? submissionsData.key
			self.helpText = nil
		}

    }


	// MARK: - Initialization
	init(_ name: String, field: inout Field?, errors: Future<[String]>?) {
		self.key = name
		self.value = field?.value
		self.label = field?.label
		self.isRequired = field?.isRequired ?? false
		self.errors = errors
	}

}

public final class FormForTag: TagRenderer {


    public func render(tag: TagContext) throws -> Future<TemplateData> {
		let submissionsDatas = try tag.submissionsDatas()

		var templateDatas: [TemplateData] = []

		for submissionsData in submissionsDatas {
			var inputPath = String()

			switch submissionsData.key {
			case "email": inputPath = "Submissions/Fields/email-input"
			case "password": inputPath = "Submissions/Fields/password-input"
			default:
				inputPath = "Submissions/Fields/input-field"
			}

			_ = (submissionsData.errors ?? tag.future([])).flatMap { errors -> Future<TemplateData> in
				let inputData =  TagContext.SubmissionsData.InputData(submissionsData, errors: errors)

				let templateData = try tag
					.container
					.make(TemplateRenderer.self)
					.render(inputPath, inputData)
					.map { view -> TemplateData in
						return TemplateData.data(view.data)
					}

					_ = templateData.map { data in
						templateDatas.append(data)
					}

				return templateData
			}
		}

		return tag.future(try templateDatas.convertToTemplateData())
    }

}

Ok so in the render method of FormForTag you don't know what kind of field you have to
render. The workaround here is to determine the template to render through the
submissionsData.key property, that's not really convenient cause a field named
'passwordConfirm' for example won't be renderer as a password field...

Solutions? Provide a way to indicate what kind of field I want directly in the
Field struct. That's not possible because you can't subclass a struct and add stored property
in extensions.

Now we know all the problems to accomplish an automatic form renderer tag.

The first idea I got was to extend Field with my own properties to store for
example an enum of FieldType (.email, .password, .number etc...)

But swift is not capable of storing property in extension
(the method with a static dictionary can't be used because of memory leaks).
Also not possible through objc_getAssociatedObject/objc_setAssociatedObject

Solutions? Provide a var customProperties: [String: AnyCodable] in Field properties.
This solution bring more than the automation but also the possibility to custom the form-input leaf files
(for example with a readonly attribute for a non-modifiable field (email of user))

This issue is more to considerate as an opneing of discussions to improve Submissions itself or it's doc too.

Thank you, I hope I was clear in my explanation...

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions