json-schema-form / angular-schema-form

Generate forms from a JSON schema, with AngularJS!
https://json-schema-form.github.io/angular-schema-form
MIT License
2.47k stars 653 forks source link

Using a separate template for read-only fields #783

Open rubenswieringa opened 7 years ago

rubenswieringa commented 7 years ago

Is there a way to use a separate template for read-only fields?

My best effort was using default-rules (schemaFormProvider.defaults) to read my global formDefaults and then point to a custom type with a separate template (see example below), but then a type defined in the form-definition can still override that.

**_(simplified example)**_ In the below snippet: - The schema-definition describes "a" as a **string** - The default-rule sets "a" its type to **read-only** - The form-definition appears to override "a" as a **textarea** ``` js // schema-definition describes "a" as a string $scope.schema = { type: 'object', properties: { a: { type: 'string' } } } // default-rule sets "a" its type to read-only schemaFormProvider.defaults.string.unshift( function ( name, schema, options ) { if ( options.global.formDefaults.readonly ) { var f = schemaFormProvider.stdFormObj( name, schema, options ); f.key = options.path; f.type = 'read-only'; // refers to read_only.html template defined through schemaFormDecoratorsProvider.defineDecorator() options.lookup[ sfPathProvider.stringify( options.path )] = f; return f; } }); // form-definition appears to override default-rule ("a" becomes a textarea) $scope.form = [ { key: 'a', type: 'textarea' } // 'textarea' seems to override 'read-only' as set by my default-rule (below) ] ``` ---

Also interesting is that although it makes sense that defaults would only apply when no type is set, the default-rule is actually called on "a" (even though it already has a type) and the result of the default-rule is disregarded. Is this expected behavior?

I'm hoping the above describes the situation clearly enough – if not I can make a plunkr.

Anthropic commented 7 years ago

@rubenswieringa I would love a plunker, not because you haven't described it well enough, you did a great job, but because it helps me to quickly confirm the behaviour and help me to help you :)

There is no built-in way to do it, but it is something I have wanted to have for a while. But as to your example I can investigate specific issue with a plunker.

rubenswieringa commented 7 years ago

@Anthropic Thanks for the quick response, here is a plunker: https://embed.plnkr.co/mCQljjSWTUUlSWPKSlhr

I noticed you attached _type:bug_ as a label, does that mean that it's not expected/intended behavior for default-rules to be overridden by types as set in the schema-definition?

Anthropic commented 7 years ago

@rubenswieringa thanks for the plunker, I see what you mean now. It isn't a bug no.

But I myself have had the problem of not being able to define a default that is not editable. I have a feeling there may already be an issue related to that.

The running of defaults each time is because it is during the step to create the defaults for the form object and it should only run if the condition is true and in many cases the test won't be purely for one attribute, I have an add-on that tests for schema type being string but then looks for a url attribute with a set of parameters that define an externally loaded schema and in that case changes the type to a dynamic select, however as I had never used any other value in the form itself I didn't notice till now that it didn't overwrite it.

Anyway based on what you are trying to do I would think that a builder would be the better option. I have another issue related to making it possible to add a builder to a select list of types, because there are plenty of other reasons to want to append/prepend a builder that applies to multiple types without the need to edit the decorators directly as you would have to now.

So for now I would edit the decorator directly until the better option is built that adds defineBuilder({in:['decorator'], append: [builder, sequence], to: [optional, type, list, to, add, to] }); to the services/decorators.js code which is what I want to do when I get time (no, I have no idea when that will be yet).

Or if you feel adventurous you could make a PR for it ;)

rubenswieringa commented 7 years ago

@Anthropic Thanks for the feedback!

the builder-approach

based on what you are trying to do I would think that a builder would be the better option

Looking through the docs, it seems that builders are intended for manipulating an existing DOM, correct? In our use-case, the read-only template is entirely different from the other templates (for one, the read-only template has no form-elements) and I don't want to pollute my other templates with the HTML from the read-only template.

So then if I were to solve this problem with a custom builder, then best option would be to make a builder that tests for read-only and (if true) replaces the original document-fragment in its entirety with the HTML from the read-only template (as loaded from $templateCache).

The only drawback in the above would be that if (let’s say) a read-only “textarea” has readOnlyBuilder, and readOnlyBuilder applies read_only.html to the document-fragment, then read_only.html might be needing certain builders that are not in the list of builders for the “textarea” template. (although this would be a minor thing that can be easily overcome)

Is this approximately what you were thinking or do you have a better approach?


for now I would edit the decorator directly until the better option is built that adds defineBuilder({in:['decorator'], append: [builder, sequence], to: [optional, type, list, to, add, to] }); to the services/decorators.js code which is what I want to do when I get time (no, I have no idea when that will be yet).

Or if you feel adventurous you could make a PR for it ;)

Always adventurous, but I'm afraid in our project we already use our own decorator/builders (we don't use any bootstrap stuff). So I can just include a custom builder in our own decorator – no?

workaround

The use-case behind the original question is mainly about the distinction between “text” and “textarea”. Both are “string” in the schema-definition, so you have to set “textarea” in the form-definition explicitly if you want a string to be a textarea. (and then this keeps my read-only default-rule from doing what I want)

A workaround would be to put some sort of property in my schema-definition that marks a field as ‘big’ text (so I don’t have to set its type to “textarea” in the form-definition). Then I can make a default-rule that reads that property and automatically marks that field as a “textarea” in my form-definition. Then if my default-rule for read-only fields is executed first, then the textarea default-rule wouldn't override it.

The best candidate I've found up to now is is string's maxLength – but if you have any better options then please do let me know.

Anthropic commented 7 years ago

@rubenswieringa partially correct, yes a builder acts upon an already defined template fragment, but it acts on it at the time of injection, so you could conceivably test for the readonly attribute and field type and then replace the fragment with another. It does mean you are acting on fragments and not on source templates, so I would like to find a way to make both work, but it would certainly be the easiest/quickest way I can think of.

rubenswieringa commented 7 years ago

@Anthropic Alright, I'll be sure to share our solution here once we've figured it out – thanks for all the help!

kyse commented 7 years ago

I'm guessing you're wanting an automatic/dynamic field rather than having to define them manually. But the manual option would be to define two fields, and only show one of them via condition.