whitecube / nova-flexible-content

Flexible Content & Repeater Fields for Laravel Nova
MIT License
780 stars 229 forks source link

Live preview #434

Open johnpuddephatt opened 1 year ago

johnpuddephatt commented 1 year ago

A bit of background to my use-case/thinking can be seen here on Twitter. In short I want to provide a visually intuitive interface for content managers when dealing with "marketing" pages on a site I'm about to start building.

Initially I was playing around with a new "preview" field that would work with Nova Flexible Content (NFC) but the extra "layer" between NFC and the fields introduced challenges, so I've looked at incorporating the functionality directly into NFC.

This is being shared as work in progress. There are still a number of things to improve/iron out, but I think the progress made is promising (or at least interesting!) and worth sharing in case anyone wants to take a look/contribute.

https://user-images.githubusercontent.com/8030910/216837001-39ac1a1a-6781-4a76-8c12-ec121f7dfaa8.mov

Benefits

Instructions for use

Enable preview on your Flexible field and provide the path to the frontend stylesheet you want your preview to use, e.g. (if using Vite):

Flexible::make('Content')
    ->enablePreview(\Illuminate\Support\Facades\Vite::asset("resources/css/app.css"))

Create a Blade view at resources/views/flexible/{LayoutName}.blade.php

Optionally define additional data you want to be passed to your layout:

public function previewData()
    {
        return [
            "posts" => \App\Models\Post::take(3)->get(),
        ];
    }

The fields defined on your layout are passed to the previewData method, so if you have a field on your layout named "Number of posts" you can do the following, which opens up some interesting possibilities:

public function previewData($parameters)
    {
        return [
            "posts" => \App\Models\Post::take($parameters["number_of_posts"])->get(),
        ];
    }

https://user-images.githubusercontent.com/8030910/216829904-2cde1573-b5c9-478d-9aa2-e5414c69ed72.mov

Limitations

To do

johnpuddephatt commented 1 year ago

When it comes to rendering on the frontend, I was imagining doing something like the following:

    @foreach ($post->flexiblecontent as $block)
        @include('flexible.' . $block->view(), $block->getAttributes())
    @endforeach

However I've realised that the "$block" above is an instance of Whitecube\NovaFlexibleContent\Layouts\Layout not my specified layout (e.g. App\Nova\Flexible\Layouts\MyLayout.php) so I can't access the "view" that is defined on my layout class.

I'm going to get around this by naming my blade templates consistently with my Flexible Layout names, as then I can do:

@foreach ($production->content as $block)
    @include('flexible.' . $block->name(), $block->getAttributes())
@endforeach

This is simpler as it doesn't require a view to be defined on each layout, instead the view name is inferred from the title. However the trade-off is it's less flexible.

EDIT: I've now updated the PR in line with the above – a (Blade) view is no longer specified on the layout; instead there must be a blade template in /resources/views/flexible with a name that matches the layout name.

voidgraphics commented 1 year ago

Thanks for the PR! This is a really cool idea. Let us know when it's ready for review.

johnpuddephatt commented 1 year ago

@voidgraphics will do! I'm using it in a new project right now which is allowing me to test it out and make improvements.

For example I've realised my introduction of a method for passing additional data to the view is redundant because accessors on the layout can be utilised instead. This means passing Layout instances through to the Blade template when creating previews rather than just raw form data... I think will be better in the long term as it keeps the preview and the frontend more closely aligned which should mean less change of them diverging.

johnpuddephatt commented 1 year ago

Still got a few things to figure out, but based on my use of this in a current project I'm continuing to evolve this and I think it's progressing into something I can see myself using a lot.

An update!

One of my initial aims was to move away from "form overload" for editors, and while my initial approach (shown in the videos above) felt like progress because it introduced content previews, in practice there were still a lot of forms to look at, and when those forms were long this was a problem because it increased the height of each layout and created something that felt quite disjointed. I considered making the form in the "sidebar" scrolling (i.e. overflow-y-auto) but this then meant that a layout with a small height would have a very small form, and scrolling elements within a scrolling page always feels a bit messy.

I was also struggling with showing the content preview and the form side-by-side on smaller screens. It felt great on my 27" external display but crowded on my 13" laptop screen.

So I've reworked the 'fullscreen' toggle I introduced to be for the whole field not for each individual layout. And I think it's working much better. Forms are only visible when the field is being viewed full screen.

In addition I've now also got previews displaying on the 'detail' view of a resource.

I'll keep refining this and if I reach a point where I think it's ready to review I'll be sure to say, but in the meantime I do welcome informal feedback from anyone interested in this!

https://user-images.githubusercontent.com/8030910/220875333-c62237d5-5cb9-45ef-9071-bd4ea2b8d9e5.mp4

johnpuddephatt commented 1 year ago

Image handling (for standard Nova Image/File/Avatar fields) is now improved. Images for use in previews are no longer stored automatically – instead you can add an imagePreviews() method to your layout class that creates the temporary file. This means you can do things like resize/crop images, which is really important for accurate layout previews as the wrong image aspect ratio can mess things up!

e.g. if I add the following method to MyFlexibleLayoutClass.php (where "main_image" is the name of the image field defined on my layout):

public function imagePreviews()
    {
        return [
            "main_image" => function ($file) {
                $filename = "temporary_uploads/" . $file->hashName() . ".jpg";
                \Illuminate\Support\Facades\Storage::disk("public")->put(
                    $filename,
                    \Intervention\Image\Facades\Image::make($file)
                        ->fit(720, 480)
                        ->encode("jpg", 75)
                );
                return $filename;
            },
           "second_image" => function($file) { .....
        ];
    }

Then the image for the field 'main_image' will be stored in a folder called temporary_uploads, resized to 720x480px.

This approach works. It doesn't feel too cumbersome and gives a lot of flexibility. However I can't decide if it's the right approach or if there's a better way, perhaps leveraging the store() method on the Image field itself. I'm not sure how straightforward this would be, and I also feel like I would need to override the storage path because I'd want to keep temporary files in one place to allow for them to be periodically cleaned up.

toonvandenbos commented 1 year ago

Hi @johnpuddephatt,

This is starting to look very nice. I really appreciate your commitment on this matter.

I will be glad to give you feedback once you're feeling ready with this. I do not have much time at the moment, so I won't be able to give you an intermediate review, sorry about that.

All I can say right now it that I'm blown away by the results you showcased in the videos. Really love the user-experience you put together with the fullscreen editor and form sidebars. I think it is a good way to handle the available space issue.

Regarding the changes made to the Layouts class in order to handle file previews, this could maybe be an interesting lead for other file-related issues on this repository. I hope I'll be able to give it a little bit more thought soon.

Thanks again for your time and work, you rock!

johnpuddephatt commented 1 year ago

@toonvandenbos thanks for the positive words! and thank you for the work on Nova Flexible Content as obviously what I've done wouldn't be possible without it.

No worries at all about you not having time to input, I run a small agency and know how tough it can be to find time for everything. There's no expectation on my part – I'd love it if this got refined and became available to more people but I'm using it right now in my own projects without issue so I'm happy.

I'm going to keep working on this as I've just used it in one project and have another starting that will involve a lot of long 'marketing' pages and I feel like this is going to work really well for it. It feels amazing getting these 'live previews' for 'free' – I'd be writing the Blade views and defining the layouts anyway, but now I get this extra functionality with (almost) no extra work.

waylay commented 1 year ago

@johnpuddephatt you rock! Just tried it today and it blew my mind! This makes the nova-flexible-content so sexy and so easy to use by non-technical people!

johnpuddephatt commented 1 year ago

@waylay awesome, thanks!

I'm moving onto a project that will make big use of this in the next couple of weeks, so I'm hoping to keep testing/improving it

FabrizioKruisland commented 1 year ago

@johnpuddephatt Do you have any update on this project?

mateusz-peczkowski commented 1 year ago

This looks incredible :) Good luck man! :)

johnpuddephatt commented 1 year ago

@FabrizioKruisland not really! I'm using it in a few projects and it's working really well, I will continue to make improvements as I use it or in response to feedback from others. I'd encourage anyone interested to install the fork and have a play around!

I think the reality is that getting this working well enough to fold it into the main package is going to be quite hard, as getting it to work consistently with every other nova package will be tough (and in some cases would require other packages to make changes at their end).

majdghithan commented 9 months ago

Hello! can you provide us with a full usage example

t1mofe1 commented 6 months ago

Yo, this is amazing bro!! Good job 🔥